diff --git a/CHANGELOG.md b/CHANGELOG.md index dc66f4b11f..99ab819e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Additions and Improvements * The EvmTool now processes State Tests from the Ethereum Reference Tests. [\#1311](https://github.com/hyperledger/besu/pull/1311) +* Experimental dns support added via the `Xdns-enabled` and `Xdns-update-enabled` CLI commands. [\#1247](https://github.com/hyperledger/besu/pull/1247) ### Bug Fixes diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java index b58af0269c..5ea854b3fa 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNode.java @@ -109,6 +109,7 @@ public class BesuNode implements NodeConfiguration, RunnableNode, AutoCloseable private final List plugins = new ArrayList<>(); private final List extraCLIOptions; private final List staticNodes; + private boolean isDnsEnabled = false; private Optional exitCode = Optional.empty(); public BesuNode( @@ -132,6 +133,7 @@ public class BesuNode implements NodeConfiguration, RunnableNode, AutoCloseable final List plugins, final List extraCLIOptions, final List staticNodes, + final boolean isDnsEnabled, final Optional privacyParameters, final List runCommand) throws IOException { @@ -174,6 +176,7 @@ public class BesuNode implements NodeConfiguration, RunnableNode, AutoCloseable }); this.extraCLIOptions = extraCLIOptions; this.staticNodes = staticNodes; + this.isDnsEnabled = isDnsEnabled; privacyParameters.ifPresent(this::setPrivacyParameters); LOG.info("Created BesuNode {}", this.toString()); } @@ -609,6 +612,10 @@ public class BesuNode implements NodeConfiguration, RunnableNode, AutoCloseable return staticNodes; } + public boolean isDnsEnabled() { + return isDnsEnabled; + } + public boolean hasStaticNodes() { return staticNodes != null && !staticNodes.isEmpty(); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index da1c78b043..b544ed666a 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -138,6 +138,13 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner { createStaticNodes(node); } + if (node.isDnsEnabled()) { + params.add("--Xdns-enabled"); + params.add("true"); + params.add("--Xdns-update-enabled"); + params.add("true"); + } + if (node.isJsonRpcEnabled()) { params.add("--rpc-http-enabled"); params.add("--rpc-http-host"); diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java index 7170896f62..10646daddc 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfiguration.java @@ -49,6 +49,7 @@ public class BesuNodeConfiguration { private final List plugins; private final List extraCLIOptions; private final List staticNodes; + private final boolean isDnsEnabled; private final Optional privacyParameters; private final List runCommand; @@ -73,6 +74,7 @@ public class BesuNodeConfiguration { final List plugins, final List extraCLIOptions, final List staticNodes, + final boolean isDnsEnabled, final Optional privacyParameters, final List runCommand) { this.name = name; @@ -95,6 +97,7 @@ public class BesuNodeConfiguration { this.plugins = plugins; this.extraCLIOptions = extraCLIOptions; this.staticNodes = staticNodes; + this.isDnsEnabled = isDnsEnabled; this.privacyParameters = privacyParameters; this.runCommand = runCommand; } @@ -179,6 +182,10 @@ public class BesuNodeConfiguration { return staticNodes; } + public boolean isDnsEnabled() { + return isDnsEnabled; + } + public Optional getPrivacyParameters() { return privacyParameters; } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java index b6951bdf04..86c780cd9d 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeConfigurationBuilder.java @@ -60,6 +60,7 @@ public class BesuNodeConfigurationBuilder { private final List plugins = new ArrayList<>(); private final List extraCLIOptions = new ArrayList<>(); private List staticNodes = new ArrayList<>(); + private boolean isDnsEnabled = false; private Optional privacyParameters = Optional.empty(); private List runCommand = new ArrayList<>(); @@ -268,6 +269,11 @@ public class BesuNodeConfigurationBuilder { return this; } + public BesuNodeConfigurationBuilder dnsEnabled(final boolean isDnsEnabled) { + this.isDnsEnabled = isDnsEnabled; + return this; + } + public BesuNodeConfigurationBuilder privacyParameters(final PrivacyParameters privacyParameters) { this.privacyParameters = Optional.ofNullable(privacyParameters); return this; @@ -300,6 +306,7 @@ public class BesuNodeConfigurationBuilder { plugins, extraCLIOptions, staticNodes, + isDnsEnabled, privacyParameters, runCommand); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java index 4f60c6acf8..2eafb47b94 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/BesuNodeFactory.java @@ -68,6 +68,7 @@ public class BesuNodeFactory { config.getPlugins(), config.getExtraCLIOptions(), config.getStaticNodes(), + config.isDnsEnabled(), config.getPrivacyParameters(), config.getRunCommand()); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/permissioning/PermissionedNodeBuilder.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/permissioning/PermissionedNodeBuilder.java index c6a246ae99..7aed397590 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/permissioning/PermissionedNodeBuilder.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/configuration/permissioning/PermissionedNodeBuilder.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.JsonRpcConfiguration; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApi; import org.hyperledger.besu.ethereum.api.jsonrpc.RpcApis; import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; import org.hyperledger.besu.ethereum.permissioning.AllowlistPersistor; import org.hyperledger.besu.ethereum.permissioning.AllowlistPersistor.ALLOWLIST_TYPE; import org.hyperledger.besu.ethereum.permissioning.LocalPermissioningConfiguration; @@ -71,6 +72,7 @@ public class PermissionedNodeBuilder { private String accountPermissioningSmartContractAddress = null; private List staticNodes = new ArrayList<>(); + private boolean isDnsEnabled = false; private boolean mining = true; public PermissionedNodeBuilder name(final String name) { @@ -139,6 +141,11 @@ public class PermissionedNodeBuilder { return this; } + public PermissionedNodeBuilder dnsEnabled(final boolean isDnsEnabled) { + this.isDnsEnabled = isDnsEnabled; + return this; + } + public PermissionedNodeBuilder disableMining() { this.mining = false; return this; @@ -188,6 +195,8 @@ public class PermissionedNodeBuilder { builder.staticNodes(staticNodes); } + builder.dnsEnabled(isDnsEnabled); + if (genesisFile != null) { builder.genesisConfigProvider((a) -> Optional.of(genesisFile)); builder.devMode(false); @@ -209,12 +218,15 @@ public class PermissionedNodeBuilder { localConfigNodesPermissioningFile = createTemporaryPermissionsFile(); } - List nodesAsListOfStrings = - localConfigPermittedNodes.stream().map(URI::toASCIIString).collect(Collectors.toList()); + final List nodeAllowList = + localConfigPermittedNodes.stream().map(EnodeURL::fromURI).collect(Collectors.toList()); + initPermissioningConfigurationFile( - ALLOWLIST_TYPE.NODES, nodesAsListOfStrings, localConfigNodesPermissioningFile); + ALLOWLIST_TYPE.NODES, + nodeAllowList.stream().map(EnodeURL::toString).collect(Collectors.toList()), + localConfigNodesPermissioningFile); - localPermissioningConfiguration.setNodeAllowlist(localConfigPermittedNodes); + localPermissioningConfiguration.setNodeAllowlist(nodeAllowList); localPermissioningConfiguration.setNodePermissioningConfigFilePath( localConfigNodesPermissioningFile.toAbsolutePath().toString()); } diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java index 17194c786d..2cbe57ff1d 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/privacy/PrivacyNode.java @@ -103,6 +103,7 @@ public class PrivacyNode implements AutoCloseable { besuConfig.getPlugins(), besuConfig.getExtraCLIOptions(), Collections.emptyList(), + besuConfig.isDnsEnabled(), besuConfig.getPrivacyParameters(), List.of()); } diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistPersistorAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistPersistorAcceptanceTest.java index 9c97eb2a59..e9c1ff12c9 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistPersistorAcceptanceTest.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistPersistorAcceptanceTest.java @@ -25,8 +25,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; +import org.web3j.protocol.exceptions.ClientConnectionException; public class AllowlistPersistorAcceptanceTest extends AcceptanceTestBase { @@ -36,6 +38,8 @@ public class AllowlistPersistorAcceptanceTest extends AcceptanceTestBase { "enode://5f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; private static final String ENODE_THREE = "enode://4f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private static final String ENODE_FOURTH = + "enode://4f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567"; private Node node; private Account senderA; @@ -99,4 +103,11 @@ public class AllowlistPersistorAcceptanceTest extends AcceptanceTestBase { perm.expectPermissioningAllowlistFileKeyValue( ALLOWLIST_TYPE.NODES, tempFile, ENODE_TWO, ENODE_ONE, ENODE_THREE)); } + + @Test + public void manipulatedNodesWhitelistWithHostnameShouldNotWorkWhenDnsDisabled() { + Assertions.assertThatThrownBy(() -> node.verify(perm.addNodesToAllowlist(ENODE_FOURTH))) + .isInstanceOf(ClientConnectionException.class) + .hasMessageContaining("Request contains an invalid node"); + } } diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistWithDnsPersistorAcceptanceTest.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistWithDnsPersistorAcceptanceTest.java new file mode 100644 index 0000000000..dc51fc5d0f --- /dev/null +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/permissioning/AllowlistWithDnsPersistorAcceptanceTest.java @@ -0,0 +1,79 @@ +/* + * Copyright ConsenSys AG. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.tests.acceptance.permissioning; + +import static org.hyperledger.besu.ethereum.permissioning.AllowlistPersistor.ALLOWLIST_TYPE; + +import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; +import org.hyperledger.besu.tests.acceptance.dsl.account.Account; +import org.hyperledger.besu.tests.acceptance.dsl.node.Node; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +public class AllowlistWithDnsPersistorAcceptanceTest extends AcceptanceTestBase { + + private static final String ENODE_ONE = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567"; + private static final String ENODE_TWO = + "enode://5f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:4567"; + private static final String ENODE_THREE = + "enode://4f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.11:4567"; + + private Node node; + private Account senderA; + private Path tempFile; + + @Before + public void setUp() throws Exception { + senderA = accounts.getPrimaryBenefactor(); + tempFile = Files.createTempFile("test", "test"); + + this.node = + permissionedNodeBuilder + .name("node") + .nodesConfigFile(tempFile) + .nodesPermittedInConfig(new ArrayList<>()) + .accountsConfigFile(tempFile) + .accountsPermittedInConfig(Collections.singletonList(senderA.getAddress())) + .dnsEnabled(true) + .build(); + + cluster.start(this.node); + } + + @Test + public void manipulatedNodesWhitelistWithHostnameShouldWorkWhenDnsEnabled() { + + node.verify(perm.addNodesToAllowlist(ENODE_ONE, ENODE_TWO)); + node.verify( + perm.expectPermissioningAllowlistFileKeyValue( + ALLOWLIST_TYPE.NODES, tempFile, ENODE_ONE, ENODE_TWO)); + + node.verify(perm.removeNodesFromAllowlist(ENODE_ONE)); + node.verify( + perm.expectPermissioningAllowlistFileKeyValue(ALLOWLIST_TYPE.NODES, tempFile, ENODE_TWO)); + + node.verify(perm.addNodesToAllowlist(ENODE_ONE, ENODE_THREE)); + node.verify( + perm.expectPermissioningAllowlistFileKeyValue( + ALLOWLIST_TYPE.NODES, tempFile, ENODE_TWO, ENODE_ONE, ENODE_THREE)); + } +} diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index ab51825343..b629b26c5a 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -95,7 +95,9 @@ import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.mainnet.precompiles.AltBN128PairingPrecompiledContract; import org.hyperledger.besu.ethereum.p2p.config.DiscoveryConfiguration; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; +import org.hyperledger.besu.ethereum.p2p.peers.ImmutableEnodeDnsConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.StaticNodesParser; import org.hyperledger.besu.ethereum.permissioning.LocalPermissioningConfiguration; import org.hyperledger.besu.ethereum.permissioning.PermissioningConfiguration; @@ -310,20 +312,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { + "Default is a predefined list.", split = ",", arity = "0..*") - void setBootnodes(final List values) { - try { - bootNodes = - values.stream() - .filter(value -> !value.isEmpty()) - .map(EnodeURL::fromString) - .collect(Collectors.toList()); - DiscoveryConfiguration.assertValidBootnodes(bootNodes); - } catch (final IllegalArgumentException e) { - throw new ParameterException(commandLine, e.getMessage()); - } - } - - private List bootNodes = null; + private final List bootNodes = null; @Option( names = {"--max-peers"}, @@ -1047,6 +1036,20 @@ public class BesuCommand implements DefaultCommandValues, Runnable { arity = "1") private final Long wsTimeoutSec = TimeoutOptions.defaultOptions().getTimeoutSeconds(); + @CommandLine.Option( + hidden = true, + names = {"--Xdns-enabled"}, + description = "Enabled DNS support", + arity = "1") + private final Boolean dnsEnabled = Boolean.FALSE; + + @CommandLine.Option( + hidden = true, + names = {"--Xdns-update-enabled"}, + description = "Allow to detect an IP update automatically", + arity = "1") + private final Boolean dnsUpdateEnabled = Boolean.FALSE; + private EthNetworkConfig ethNetworkConfig; private JsonRpcConfiguration jsonRpcConfiguration; private GraphQLConfiguration graphQLConfiguration; @@ -1059,6 +1062,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { private final Supplier metricsSystem = Suppliers.memoize(() -> PrometheusMetricsSystem.init(metricsConfiguration())); private Vertx vertx; + private EnodeDnsConfiguration enodeDnsConfiguration; public BesuCommand( final Logger logger, @@ -1329,6 +1333,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { validateMiningParams(); validateNatParams(); validateNetStatsParams(); + validateDnsOptionsParams(); return this; } @@ -1387,6 +1392,15 @@ public class BesuCommand implements DefaultCommandValues, Runnable { } } + private void validateDnsOptionsParams() { + if (!dnsEnabled && dnsUpdateEnabled) { + throw new ParameterException( + this.commandLine, + "The `--Xdns-update-enabled` requires dns to be enabled. Either remove --Xdns-update-enabled" + + " or specify dns is enabled (--Xdns-enabled)"); + } + } + private void issueOptionWarnings() { // Check that P2P options are able to work CommandLineUtils.checkOptionDependencies( @@ -1441,26 +1455,25 @@ public class BesuCommand implements DefaultCommandValues, Runnable { genesisFile == null && !isPrivacyEnabled && network != NetworkName.DEV ? SyncMode.FAST : SyncMode.FULL); + ethNetworkConfig = updateNetworkConfig(getNetwork()); jsonRpcConfiguration = jsonRpcConfiguration(); graphQLConfiguration = graphQLConfiguration(); webSocketConfiguration = webSocketConfiguration(); + permissioningConfiguration = permissioningConfiguration(); staticNodes = loadStaticNodes(); + logger.info("Connecting to {} static nodes.", staticNodes.size()); logger.trace("Static Nodes = {}", staticNodes); - final List enodeURIs = - ethNetworkConfig.getBootNodes().stream().map(EnodeURL::toURI).collect(Collectors.toList()); + final List enodeURIs = ethNetworkConfig.getBootNodes(); permissioningConfiguration .flatMap(PermissioningConfiguration::getLocalConfig) .ifPresent(p -> ensureAllNodesAreInAllowlist(enodeURIs, p)); permissioningConfiguration .flatMap(PermissioningConfiguration::getLocalConfig) - .ifPresent( - p -> - ensureAllNodesAreInAllowlist( - staticNodes.stream().map(EnodeURL::toURI).collect(Collectors.toList()), p)); + .ifPresent(p -> ensureAllNodesAreInAllowlist(staticNodes, p)); metricsConfiguration = metricsConfiguration(); logger.info("Security Module: {}", securityModuleName); @@ -1474,7 +1487,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { } private void ensureAllNodesAreInAllowlist( - final Collection enodeAddresses, + final Collection enodeAddresses, final LocalPermissioningConfiguration permissioningConfiguration) { try { PermissioningConfigurationValidator.areAllNodesAreInAllowlist( @@ -1781,6 +1794,7 @@ public class BesuCommand implements DefaultCommandValues, Runnable { final LocalPermissioningConfiguration localPermissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( permissionsNodesEnabled, + getEnodeDnsConfiguration(), nodePermissioningConfigFile.orElse(getDefaultPermissioningFilePath()), permissionsAccountsEnabled, accountPermissioningConfigFile.orElse(getDefaultPermissioningFilePath())); @@ -2189,7 +2203,18 @@ public class BesuCommand implements DefaultCommandValues, Runnable { } if (bootNodes != null) { - builder.setBootNodes(bootNodes); + try { + + final List listBootNodes = + bootNodes.stream() + .filter(value -> !value.isEmpty()) + .map(url -> EnodeURL.fromString(url, getEnodeDnsConfiguration())) + .collect(Collectors.toList()); + DiscoveryConfiguration.assertValidBootnodes(listBootNodes); + builder.setBootNodes(listBootNodes); + } catch (final IllegalArgumentException e) { + throw new ParameterException(commandLine, e.getMessage()); + } } return builder.build(); @@ -2267,13 +2292,24 @@ public class BesuCommand implements DefaultCommandValues, Runnable { final String staticNodesFilename = "static-nodes.json"; final Path staticNodesPath = dataDir().resolve(staticNodesFilename); - return StaticNodesParser.fromPath(staticNodesPath); + return StaticNodesParser.fromPath(staticNodesPath, getEnodeDnsConfiguration()); } public BesuExceptionHandler exceptionHandler() { return new BesuExceptionHandler(this::getLogLevel); } + public EnodeDnsConfiguration getEnodeDnsConfiguration() { + if (enodeDnsConfiguration == null) { + enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(dnsEnabled) + .updateEnabled(dnsUpdateEnabled) + .build(); + } + return enodeDnsConfiguration; + } + @VisibleForTesting Level getLogLevel() { return logLevel; diff --git a/besu/src/main/java/org/hyperledger/besu/util/PermissioningConfigurationValidator.java b/besu/src/main/java/org/hyperledger/besu/util/PermissioningConfigurationValidator.java index 3d0f269212..2c1898cdff 100644 --- a/besu/src/main/java/org/hyperledger/besu/util/PermissioningConfigurationValidator.java +++ b/besu/src/main/java/org/hyperledger/besu/util/PermissioningConfigurationValidator.java @@ -14,10 +14,12 @@ */ package org.hyperledger.besu.util; +import static java.util.stream.Collectors.toList; + +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; import org.hyperledger.besu.ethereum.permissioning.LocalPermissioningConfiguration; import java.net.URI; -import java.net.URISyntaxException; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @@ -25,21 +27,21 @@ import java.util.stream.Collectors; public class PermissioningConfigurationValidator { public static void areAllNodesAreInAllowlist( - final Collection nodeURIs, + final Collection nodeURIs, final LocalPermissioningConfiguration permissioningConfiguration) throws Exception { if (permissioningConfiguration.isNodeAllowlistEnabled() && nodeURIs != null) { final List allowlistNodesWithoutQueryParam = permissioningConfiguration.getNodeAllowlist().stream() - .map(PermissioningConfigurationValidator::removeQueryFromURI) - .collect(Collectors.toList()); + .map(EnodeURL::toURIWithoutDiscoveryPort) + .collect(toList()); final List nodeURIsNotInAllowlist = nodeURIs.stream() - .map(PermissioningConfigurationValidator::removeQueryFromURI) + .map(EnodeURL::toURIWithoutDiscoveryPort) .filter(uri -> !allowlistNodesWithoutQueryParam.contains(uri)) - .collect(Collectors.toList()); + .collect(toList()); if (!nodeURIsNotInAllowlist.isEmpty()) { throw new Exception( @@ -48,21 +50,6 @@ public class PermissioningConfigurationValidator { } } - private static URI removeQueryFromURI(final URI uri) { - try { - return new URI( - uri.getScheme(), - uri.getUserInfo(), - uri.getHost(), - uri.getPort(), - uri.getPath(), - null, - uri.getFragment()); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - } - private static Collection enodesAsStrings(final List enodes) { return enodes.parallelStream().map(URI::toASCIIString).collect(Collectors.toList()); } diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 8138e70634..382be475f6 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -590,11 +590,11 @@ public class BesuCommandTest extends CommandTestAbstract { @Test public void nodePermissioningTomlPathMustUseOption() throws IOException { - final List allowedNodes = + final List allowedNodes = Lists.newArrayList( - URI.create( + EnodeURL.fromString( "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567"), - URI.create( + EnodeURL.fromString( "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.169.0.9:4568")); final URL configFile = this.getClass().getResource(PERMISSIONING_CONFIG_TOML); @@ -1347,6 +1347,33 @@ public class BesuCommandTest extends CommandTestAbstract { "The `--Xethstats-contact` requires ethstats server URL to be provided. Either remove --Xethstats-contact or provide an url (via --Xethstats=nodename:secret@host:port)"); } + @Test + public void dnsEnabledOptionIsParsedCorrectly() { + TestBesuCommand besuCommand = parseCommand("--Xdns-enabled", "true"); + + assertThat(besuCommand.getEnodeDnsConfiguration().dnsEnabled()).isTrue(); + assertThat(besuCommand.getEnodeDnsConfiguration().updateEnabled()).isFalse(); + } + + @Test + public void dnsUpdateEnabledOptionIsParsedCorrectly() { + TestBesuCommand besuCommand = + parseCommand("--Xdns-enabled", "true", "--Xdns-update-enabled", "true"); + + assertThat(besuCommand.getEnodeDnsConfiguration().dnsEnabled()).isTrue(); + assertThat(besuCommand.getEnodeDnsConfiguration().updateEnabled()).isTrue(); + } + + @Test + public void dnsUpdateEnabledOptionCannotBeUsedWithoutDnsEnabled() { + parseCommand("--Xdns-update-enabled", "true"); + Mockito.verifyZeroInteractions(mockRunnerBuilder); + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()) + .contains( + "The `--Xdns-update-enabled` requires dns to be enabled. Either remove --Xdns-update-enabled or specify dns is enabled (--Xdns-enabled)"); + } + @Test public void helpShouldDisplayNatMethodInfo() { parseCommand("--help"); diff --git a/besu/src/test/java/org/hyperledger/besu/util/LocalPermissioningConfigurationValidatorTest.java b/besu/src/test/java/org/hyperledger/besu/util/LocalPermissioningConfigurationValidatorTest.java index 63b80138c4..e46e10ac3e 100644 --- a/besu/src/test/java/org/hyperledger/besu/util/LocalPermissioningConfigurationValidatorTest.java +++ b/besu/src/test/java/org/hyperledger/besu/util/LocalPermissioningConfigurationValidatorTest.java @@ -15,20 +15,21 @@ package org.hyperledger.besu.util; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import org.hyperledger.besu.cli.config.EthNetworkConfig; import org.hyperledger.besu.cli.config.NetworkName; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; +import org.hyperledger.besu.ethereum.p2p.peers.ImmutableEnodeDnsConfiguration; import org.hyperledger.besu.ethereum.permissioning.LocalPermissioningConfiguration; import org.hyperledger.besu.ethereum.permissioning.PermissioningConfigurationBuilder; -import java.net.URI; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.stream.Collectors; import com.google.common.collect.Lists; import com.google.common.io.Resources; @@ -39,6 +40,10 @@ public class LocalPermissioningConfigurationValidatorTest { static final String PERMISSIONING_CONFIG_ROPSTEN_BOOTNODES = "/permissioning_config_ropsten_bootnodes.toml"; static final String PERMISSIONING_CONFIG = "/permissioning_config.toml"; + static final String PERMISSIONING_CONFIG_VALID_HOSTNAME = + "/permissioning_config_valid_hostname.toml"; + static final String PERMISSIONING_CONFIG_UNKNOWN_HOSTNAME = + "/permissioning_config_unknown_hostname.toml"; @Test public void ropstenWithNodesAllowlistOptionWhichDoesIncludeRopstenBootnodesMustNotError() @@ -52,10 +57,13 @@ public class LocalPermissioningConfigurationValidatorTest { LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), true, toml.toAbsolutePath().toString()); + true, + EnodeDnsConfiguration.DEFAULT_CONFIG, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); - final List enodeURIs = - ethNetworkConfig.getBootNodes().stream().map(EnodeURL::toURI).collect(Collectors.toList()); + final List enodeURIs = ethNetworkConfig.getBootNodes(); PermissioningConfigurationValidator.areAllNodesAreInAllowlist( enodeURIs, permissioningConfiguration); } @@ -72,13 +80,14 @@ public class LocalPermissioningConfigurationValidatorTest { LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), true, toml.toAbsolutePath().toString()); + true, + EnodeDnsConfiguration.DEFAULT_CONFIG, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); try { - final List enodeURIs = - ethNetworkConfig.getBootNodes().stream() - .map(EnodeURL::toURI) - .collect(Collectors.toList()); + final List enodeURIs = ethNetworkConfig.getBootNodes(); PermissioningConfigurationValidator.areAllNodesAreInAllowlist( enodeURIs, permissioningConfiguration); fail("expected exception because ropsten bootnodes are not in node-allowlist"); @@ -102,11 +111,15 @@ public class LocalPermissioningConfigurationValidatorTest { final LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), true, toml.toAbsolutePath().toString()); + true, + EnodeDnsConfiguration.DEFAULT_CONFIG, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); // This node is defined in the PERMISSIONING_CONFIG file without the discovery port - final URI enodeURL = - URI.create( + final EnodeURL enodeURL = + EnodeURL.fromString( "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.9:4567?discport=30303"); // In an URI comparison the URLs should not match @@ -123,4 +136,89 @@ public class LocalPermissioningConfigurationValidatorTest { "Exception not expected. Validation of nodes in allowlist should ignore the optional discovery port param."); } } + + @Test + public void nodeAllowlistCheckShouldWorkWithHostnameIfDnsEnabled() throws Exception { + final URL configFile = this.getClass().getResource(PERMISSIONING_CONFIG_VALID_HOSTNAME); + final Path toml = Files.createTempFile("toml", ""); + toml.toFile().deleteOnExit(); + Files.write(toml, Resources.toByteArray(configFile)); + + final ImmutableEnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build(); + final LocalPermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration( + true, + enodeDnsConfiguration, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); + + // This node is defined in the PERMISSIONING_CONFIG_DNS file without the discovery port + final EnodeURL enodeURL = + EnodeURL.fromString( + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567?discport=30303", + enodeDnsConfiguration); + + // In an URI comparison the URLs should not match + boolean isInAllowlist = permissioningConfiguration.getNodeAllowlist().contains(enodeURL); + assertThat(isInAllowlist).isFalse(); + + // However, for the allowlist validation, we should ignore the discovery port and don't throw an + // error + try { + PermissioningConfigurationValidator.areAllNodesAreInAllowlist( + Lists.newArrayList(enodeURL), permissioningConfiguration); + } catch (Exception e) { + fail( + "Exception not expected. Validation of nodes in allowlist should ignore the optional discovery port param."); + } + } + + @Test + public void nodeAllowlistCheckShouldNotWorkWithHostnameWhenDnsDisabled() throws Exception { + final URL configFile = this.getClass().getResource(PERMISSIONING_CONFIG_VALID_HOSTNAME); + final Path toml = Files.createTempFile("toml", ""); + toml.toFile().deleteOnExit(); + Files.write(toml, Resources.toByteArray(configFile)); + + final ImmutableEnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(false).updateEnabled(false).build(); + + assertThatThrownBy( + () -> + PermissioningConfigurationBuilder.permissioningConfiguration( + true, + enodeDnsConfiguration, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Invalid enode URL syntax. Enode URL should have the following format 'enode://@:[?discport=]'. Invalid ip address."); + } + + @Test + public void nodeAllowlistCheckShouldNotWorkWithUnknownHostnameWhenOnlyDnsEnabled() + throws Exception { + final URL configFile = this.getClass().getResource(PERMISSIONING_CONFIG_UNKNOWN_HOSTNAME); + final Path toml = Files.createTempFile("toml", ""); + toml.toFile().deleteOnExit(); + Files.write(toml, Resources.toByteArray(configFile)); + + final ImmutableEnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(false).updateEnabled(false).build(); + + assertThatThrownBy( + () -> + PermissioningConfigurationBuilder.permissioningConfiguration( + true, + enodeDnsConfiguration, + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Invalid enode URL syntax. Enode URL should have the following format 'enode://@:[?discport=]'. Invalid ip address."); + } } diff --git a/besu/src/test/resources/permissioning_config_unknown_hostname.toml b/besu/src/test/resources/permissioning_config_unknown_hostname.toml new file mode 100644 index 0000000000..d46e4b3549 --- /dev/null +++ b/besu/src/test/resources/permissioning_config_unknown_hostname.toml @@ -0,0 +1,4 @@ +# Permissioning TOML file + +accounts-allowlist=["0x0000000000000000000000000000000000000009"] +nodes-allowlist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@hostname:4567","enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4568"] diff --git a/besu/src/test/resources/permissioning_config_valid_hostname.toml b/besu/src/test/resources/permissioning_config_valid_hostname.toml new file mode 100644 index 0000000000..92d520b747 --- /dev/null +++ b/besu/src/test/resources/permissioning_config_valid_hostname.toml @@ -0,0 +1,4 @@ +# Permissioning TOML file + +accounts-allowlist=["0x0000000000000000000000000000000000000009"] +nodes-allowlist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567","enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4568"] diff --git a/ethereum/p2p/build.gradle b/ethereum/p2p/build.gradle index 21ef7a49eb..f683326e1f 100644 --- a/ethereum/p2p/build.gradle +++ b/ethereum/p2p/build.gradle @@ -42,6 +42,9 @@ dependencies { implementation 'org.apache.tuweni:tuweni-units' implementation 'org.xerial.snappy:snappy-java' + annotationProcessor "org.immutables:value" + implementation "org.immutables:value-annotations" + runtimeOnly 'org.apache.logging.log4j:log4j-core' // test dependencies. diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeDnsConfiguration.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeDnsConfiguration.java new file mode 100644 index 0000000000..1ee49120ea --- /dev/null +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeDnsConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright ConsenSys AG. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.p2p.peers; + +import org.immutables.value.Value; + +@Value.Immutable +public interface EnodeDnsConfiguration { + + EnodeDnsConfiguration DEFAULT_CONFIG = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(false).updateEnabled(false).build(); + + static EnodeDnsConfiguration dnsDisabled() { + return DEFAULT_CONFIG; + } + + boolean dnsEnabled(); + + boolean updateEnabled(); +} diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURL.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURL.java index 899066e3e8..0d4e592b15 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURL.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURL.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.util.NetworkUtility; import java.net.InetAddress; import java.net.URI; +import java.net.UnknownHostException; import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; @@ -40,13 +41,15 @@ public class EnodeURL { private static final Pattern NODE_ID_PATTERN = Pattern.compile("^[0-9a-fA-F]{128}$"); private final Bytes nodeId; - private final InetAddress ip; + private InetAddress ip; + private final Optional maybeHostname; private final Optional listeningPort; private final Optional discoveryPort; private EnodeURL( final Bytes nodeId, final InetAddress address, + final Optional maybeHostname, final Optional listeningPort, final Optional discoveryPort) { checkArgument( @@ -56,6 +59,7 @@ public class EnodeURL { this.nodeId = nodeId; this.ip = address; + this.maybeHostname = maybeHostname; this.listeningPort = listeningPort; this.discoveryPort = discoveryPort; } @@ -65,9 +69,14 @@ public class EnodeURL { } public static EnodeURL fromString(final String value) { + return fromString(value, EnodeDnsConfiguration.dnsDisabled()); + } + + public static EnodeURL fromString( + final String value, final EnodeDnsConfiguration enodeDnsConfiguration) { try { checkStringArgumentNotEmpty(value, "Invalid empty value."); - return fromURI(URI.create(value)); + return fromURI(URI.create(value), enodeDnsConfiguration); } catch (IllegalArgumentException e) { String message = "Invalid enode URL syntax. Enode URL should have the following format 'enode://@:[?discport=]'."; @@ -79,6 +88,10 @@ public class EnodeURL { } public static EnodeURL fromURI(final URI uri) { + return fromURI(uri, EnodeDnsConfiguration.dnsDisabled()); + } + + public static EnodeURL fromURI(final URI uri, final EnodeDnsConfiguration enodeDnsConfiguration) { checkArgument(uri != null, "URI cannot be null"); checkStringArgumentNotEmpty(uri.getScheme(), "Missing 'enode' scheme."); checkStringArgumentNotEmpty(uri.getHost(), "Missing or invalid ip address."); @@ -108,7 +121,7 @@ public class EnodeURL { } return builder() - .ipAddress(host) + .ipAddress(host, enodeDnsConfiguration) .nodeId(id) .listeningPort(tcpPort) .discoveryPort(discoveryPort) @@ -124,9 +137,9 @@ public class EnodeURL { return false; } - return Objects.equals(enodeA.nodeId, enodeB.nodeId) - && Objects.equals(enodeA.ip, enodeB.ip) - && Objects.equals(enodeA.listeningPort, enodeB.listeningPort); + return Objects.equals(enodeA.getNodeId(), enodeB.getNodeId()) + && Objects.equals(enodeA.getIp(), enodeB.getIp()) + && Objects.equals(enodeA.getListeningPort(), enodeB.getListeningPort()); } public static Bytes parseNodeId(final String nodeId) { @@ -141,11 +154,12 @@ public class EnodeURL { } public URI toURI() { + final String uri = String.format( "enode://%s@%s:%d", nodeId.toUnprefixedHexString(), - InetAddresses.toUriString(ip), + maybeHostname.orElse(InetAddresses.toUriString(getIp())), getListeningPortOrZero()); final OptionalInt discPort = getDiscPortQueryParam(); if (discPort.isPresent()) { @@ -160,7 +174,7 @@ public class EnodeURL { String.format( "enode://%s@%s:%d", nodeId.toUnprefixedHexString(), - InetAddresses.toUriString(ip), + maybeHostname.orElse(InetAddresses.toUriString(getIp())), getListeningPortOrZero()); return URI.create(uri); @@ -181,7 +195,11 @@ public class EnodeURL { } public static URI asURI(final String url) { - return fromString(url).toURI(); + return asURI(url, EnodeDnsConfiguration.dnsDisabled()); + } + + public static URI asURI(final String url, final EnodeDnsConfiguration enodeDnsConfiguration) { + return fromString(url, enodeDnsConfiguration).toURI(); } public Bytes getNodeId() { @@ -189,10 +207,32 @@ public class EnodeURL { } public String getIpAsString() { - return ip.getHostAddress(); + return getIp().getHostAddress(); } + /** + * Get IP of the EnodeURL + * + *

If "dns" and "dns-update" are enabled -> DNS lookup every time to have the IP up to date + * and not to rely on an invalid cache + * + *

If the "dns" is enabled but "dns-update" is disabled -> IP is retrieved only one time and + * the hostname is no longer stored (maybeHostname is empty). + * + * @return ip + */ public InetAddress getIp() { + this.ip = + maybeHostname + .map( + hostname -> { + try { + return InetAddress.getByName(hostname); + } catch (UnknownHostException e) { + return ip; + } + }) + .orElse(ip); return ip; } @@ -229,15 +269,15 @@ public class EnodeURL { return false; } EnodeURL enodeURL = (EnodeURL) o; - return Objects.equals(nodeId, enodeURL.nodeId) - && Objects.equals(ip, enodeURL.ip) - && Objects.equals(listeningPort, enodeURL.listeningPort) - && Objects.equals(discoveryPort, enodeURL.discoveryPort); + return Objects.equals(getNodeId(), enodeURL.getNodeId()) + && Objects.equals(getIp(), enodeURL.getIp()) + && Objects.equals(getListeningPort(), enodeURL.getListeningPort()) + && Objects.equals(getDiscoveryPort(), enodeURL.getDiscoveryPort()); } @Override public int hashCode() { - return Objects.hash(nodeId, ip, listeningPort, discoveryPort); + return Objects.hash(getNodeId(), getIp(), getListeningPort(), getDiscoveryPort()); } @Override @@ -250,13 +290,14 @@ public class EnodeURL { private Bytes nodeId; private Optional listeningPort; private Optional discoveryPort; + private Optional maybeHostname = Optional.empty(); private InetAddress ip; private Builder() {} public EnodeURL build() { validate(); - return new EnodeURL(nodeId, ip, listeningPort, discoveryPort); + return new EnodeURL(nodeId, ip, maybeHostname, listeningPort, discoveryPort); } private void validate() { @@ -294,10 +335,27 @@ public class EnodeURL { } public Builder ipAddress(final String ip) { + return ipAddress(ip, EnodeDnsConfiguration.dnsDisabled()); + } + + public Builder ipAddress(final String ip, final EnodeDnsConfiguration enodeDnsConfiguration) { if (InetAddresses.isUriInetAddress(ip)) { this.ip = InetAddresses.forUriString(ip); } else if (InetAddresses.isInetAddress(ip)) { this.ip = InetAddresses.forString(ip); + } else if (enodeDnsConfiguration.dnsEnabled()) { + try { + if (enodeDnsConfiguration.updateEnabled()) { + this.maybeHostname = Optional.of(ip); + } + this.ip = InetAddress.getByName(ip); + } catch (UnknownHostException e) { + if (!enodeDnsConfiguration.updateEnabled()) { + throw new IllegalArgumentException("Invalid ip address or hostname."); + } else { + this.ip = InetAddresses.forString("127.0.0.1"); + } + } } else { throw new IllegalArgumentException("Invalid ip address."); } diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParser.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParser.java index 7c24bd00fc..28b7ff80bc 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParser.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParser.java @@ -35,11 +35,12 @@ public class StaticNodesParser { private static final Logger LOG = LogManager.getLogger(); - public static Set fromPath(final Path path) + public static Set fromPath( + final Path path, final EnodeDnsConfiguration enodeDnsConfiguration) throws IOException, IllegalArgumentException { try { - return readEnodesFromPath(path); + return readEnodesFromPath(path, enodeDnsConfiguration); } catch (FileNotFoundException | NoSuchFileException ex) { LOG.info("StaticNodes file {} does not exist, no static connections will be created.", path); return emptySet(); @@ -55,7 +56,8 @@ public class StaticNodesParser { } } - private static Set readEnodesFromPath(final Path path) throws IOException { + private static Set readEnodesFromPath( + final Path path, final EnodeDnsConfiguration enodeDnsConfiguration) throws IOException { final byte[] staticNodesContent = Files.readAllBytes(path); if (staticNodesContent.length == 0) { return emptySet(); @@ -63,13 +65,14 @@ public class StaticNodesParser { final JsonArray enodeJsonArray = new JsonArray(new String(staticNodesContent, UTF_8)); return enodeJsonArray.stream() - .map(obj -> decodeString((String) obj)) + .map(obj -> decodeString((String) obj, enodeDnsConfiguration)) .collect(Collectors.toSet()); } - private static EnodeURL decodeString(final String input) { + private static EnodeURL decodeString( + final String input, final EnodeDnsConfiguration enodeDnsConfiguration) { try { - final EnodeURL enode = EnodeURL.fromString(input); + final EnodeURL enode = EnodeURL.fromString(input, enodeDnsConfiguration); checkArgument( enode.isListening(), "Static node must be configured with a valid listening port."); return enode; diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURLTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURLTest.java index c0639451b4..edc1f1d45a 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURLTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/EnodeURLTest.java @@ -376,6 +376,82 @@ public class EnodeURLTest { "Invalid node ID: node ID must have exactly 128 hexadecimal characters and should not include any '0x' hex prefix."); } + @Test + public void fromString_withHostnameEnodeURLShouldFailWhenDnsDisabled() { + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + + assertThatThrownBy( + () -> + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(false) + .updateEnabled(false) + .build())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid enode URL syntax"); + } + + @Test + public void fromString_withHostnameEnodeURLShouldFailWhenDnsDisabledAndUpdateEnabled() { + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + + assertThatThrownBy( + () -> + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(false) + .updateEnabled(true) + .build())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid enode URL syntax"); + } + + @Test + public void fromString_withHostnameEnodeURLShouldWorkWhenDnsEnabled() { + final EnodeURL expectedEnodeURL = + EnodeURL.builder() + .nodeId(VALID_NODE_ID) + .ipAddress("127.0.0.1") + .listeningPort(P2P_PORT) + .discoveryPort(Optional.of(DISCOVERY_PORT)) + .build(); + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + + final EnodeURL enodeURL = + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build()); + ; + + assertThat(enodeURL).isEqualTo(expectedEnodeURL); + } + + @Test + public void fromString_withHostnameEnodeURLShouldWorkWhenDnsEnabledAndUpdateEnabled() { + final EnodeURL expectedEnodeURL = + EnodeURL.builder() + .nodeId(VALID_NODE_ID) + .ipAddress("127.0.0.1") + .listeningPort(P2P_PORT) + .discoveryPort(Optional.of(DISCOVERY_PORT)) + .build(); + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + + final EnodeURL enodeURL = + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build()); + ; + + assertThat(enodeURL).isEqualTo(expectedEnodeURL); + } + @Test public void toURI_WithDiscoveryPortCreateExpectedURI() { final String enodeURLString = @@ -395,6 +471,78 @@ public class EnodeURLTest { assertThat(createdURI).isEqualTo(expectedURI); } + @Test + public void toURI_WithHostnameShouldWorkWhenDnsEnabled() { + final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT; + final URI expectedURI = + URI.create("enode://" + VALID_NODE_ID + "@" + "127.0.0.1" + ":" + P2P_PORT); + final URI createdURI = + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(true) + .updateEnabled(false) + .build()) + .toURI(); + + assertThat(createdURI).isEqualTo(expectedURI); + } + + @Test + public void toURI_WithHostnameShouldWorkWhenDnsEnabledAndUpdateEnabled() { + final String enodeURLString = "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT; + final URI expectedURI = URI.create(enodeURLString); + final URI createdURI = + EnodeURL.fromString( + enodeURLString, + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(true) + .updateEnabled(true) + .build()) + .toURI(); + + assertThat(createdURI).isEqualTo(expectedURI); + } + + @Test + public void fromURI_withHostnameShouldFailWhenDnsDisabled() { + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + final URI expectedURI = URI.create(enodeURLString); + + assertThatThrownBy( + () -> + EnodeURL.fromURI( + expectedURI, + ImmutableEnodeDnsConfiguration.builder() + .dnsEnabled(false) + .updateEnabled(false) + .build())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid ip address"); + } + + @Test + public void fromURI_withHostnameEnodeURLShouldWorkWhenDnsEnabled() { + final EnodeURL expectedEnodeURL = + EnodeURL.builder() + .nodeId(VALID_NODE_ID) + .ipAddress("127.0.0.1") + .listeningPort(P2P_PORT) + .discoveryPort(Optional.of(DISCOVERY_PORT)) + .build(); + + final String enodeURLString = + "enode://" + VALID_NODE_ID + "@" + "localhost" + ":" + P2P_PORT + "?" + DISCOVERY_QUERY; + final URI expectedURI = URI.create(enodeURLString); + + final EnodeURL enodeURL = + EnodeURL.fromURI( + expectedURI, + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build()); + assertThat(enodeURL).isEqualTo(expectedEnodeURL); + } + @Test public void builder_setInvalidPorts() { final EnodeURL.Builder validBuilder = diff --git a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParserTest.java b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParserTest.java index 8b8aedc121..8ade4837a7 100644 --- a/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParserTest.java +++ b/ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/peers/StaticNodesParserTest.java @@ -73,18 +73,80 @@ public class StaticNodesParserTest { public void validFileLoadsWithExpectedEnodes() throws IOException, URISyntaxException { final URL resource = StaticNodesParserTest.class.getResource("valid_static_nodes.json"); final File validFile = new File(resource.getFile()); - final Set enodes = StaticNodesParser.fromPath(validFile.toPath()); + final Set enodes = + StaticNodesParser.fromPath(validFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG); assertThat(enodes) .containsExactlyInAnyOrder(validFileItems.toArray(new EnodeURL[validFileItems.size()])); } + @Test + public void validFileLoadsWithExpectedEnodesWhenDnsEnabled() + throws IOException, URISyntaxException { + final URL resource = + StaticNodesParserTest.class.getResource("valid_hostname_static_nodes.json"); + final File validFile = new File(resource.getFile()); + final EnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build(); + final Set enodes = + StaticNodesParser.fromPath(validFile.toPath(), enodeDnsConfiguration); + + assertThat(enodes) + .containsExactlyInAnyOrder(validFileItems.toArray(new EnodeURL[validFileItems.size()])); + } + + @Test + public void fileWithHostnameThrowsAnExceptionWhenDnsDisabled() + throws IOException, URISyntaxException { + final URL resource = + StaticNodesParserTest.class.getResource("valid_hostname_static_nodes.json"); + final File invalidFile = new File(resource.getFile()); + + assertThatThrownBy( + () -> + StaticNodesParser.fromPath( + invalidFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void fileWithUnknownHostnameNotThrowsAnExceptionWhenDnsAndUpdateEnabled() + throws IOException, URISyntaxException { + final URL resource = + StaticNodesParserTest.class.getResource("unknown_hostname_static_nodes.json"); + final File validFile = new File(resource.getFile()); + final EnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(true).build(); + final Set enodes = + StaticNodesParser.fromPath(validFile.toPath(), enodeDnsConfiguration); + + assertThat(enodes) + .containsExactlyInAnyOrder(validFileItems.toArray(new EnodeURL[validFileItems.size()])); + } + + @Test + public void fileWithUnknownHostnameThrowsAnExceptionWhenOnlyDnsEnabled() + throws IOException, URISyntaxException { + final URL resource = + StaticNodesParserTest.class.getResource("unknown_hostname_static_nodes.json"); + final File invalidFile = new File(resource.getFile()); + final EnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build(); + + assertThatThrownBy( + () -> StaticNodesParser.fromPath(invalidFile.toPath(), enodeDnsConfiguration)) + .isInstanceOf(IllegalArgumentException.class); + } + @Test public void invalidFileThrowsAnException() { final URL resource = StaticNodesParserTest.class.getResource("invalid_static_nodes.json"); final File invalidFile = new File(resource.getFile()); - assertThatThrownBy(() -> StaticNodesParser.fromPath(invalidFile.toPath())) + assertThatThrownBy( + () -> + StaticNodesParser.fromPath( + invalidFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG)) .isInstanceOf(IllegalArgumentException.class); } @@ -94,7 +156,10 @@ public class StaticNodesParserTest { StaticNodesParserTest.class.getResource("invalid_static_nodes_no_listening_port.json"); final File invalidFile = new File(resource.getFile()); - assertThatThrownBy(() -> StaticNodesParser.fromPath(invalidFile.toPath())) + assertThatThrownBy( + () -> + StaticNodesParser.fromPath( + invalidFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Static node must be configured with a valid listening port"); } @@ -105,7 +170,9 @@ public class StaticNodesParserTest { tempFile.deleteOnExit(); Files.write(tempFile.toPath(), "This Is Not Json".getBytes(Charset.forName("UTF-8"))); - assertThatThrownBy(() -> StaticNodesParser.fromPath(tempFile.toPath())) + assertThatThrownBy( + () -> + StaticNodesParser.fromPath(tempFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG)) .isInstanceOf(DecodeException.class); } @@ -113,7 +180,8 @@ public class StaticNodesParserTest { public void anEmptyCacheIsCreatedIfTheFileDoesNotExist() throws IOException { final Path path = Paths.get("./arbirtraryFilename.txt"); - final Set enodes = StaticNodesParser.fromPath(path); + final Set enodes = + StaticNodesParser.fromPath(path, EnodeDnsConfiguration.DEFAULT_CONFIG); assertThat(enodes.size()).isZero(); } @@ -122,7 +190,8 @@ public class StaticNodesParserTest { final File tempFile = testFolder.newFile("file.txt"); tempFile.deleteOnExit(); - final Set enodes = StaticNodesParser.fromPath(tempFile.toPath()); + final Set enodes = + StaticNodesParser.fromPath(tempFile.toPath(), EnodeDnsConfiguration.DEFAULT_CONFIG); assertThat(enodes.size()).isZero(); } } diff --git a/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/unknown_hostname_static_nodes.json b/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/unknown_hostname_static_nodes.json new file mode 100644 index 0000000000..42eb845b7c --- /dev/null +++ b/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/unknown_hostname_static_nodes.json @@ -0,0 +1,4 @@ +["enode://50203c6bfca6874370e71aecc8958529fd723feb05013dc1abca8fc1fff845c5259faba05852e9dfe5ce172a7d6e7c2a3a5eaa8b541c8af15ea5518bbff5f2fa@localhost:30303", + "enode://02beb46bc17227616be44234071dfa18516684e45eed88049190b6cb56b0bae218f045fd0450f123b8f55c60b96b78c45e8e478004293a8de6818aa4e02eff97@nodfound:30304", + "enode://819e5cbd81f123516b10f04bf620daa2b385efef06d77253148b814bf1bb6197ff58ebd1fd7bf5dc765b49a4440c733bf941e479c800173f2bfeb887e4fbcbc2@127.0.0.1:30305", + "enode://6cf53e25d2a98a22e7e205a86bda7077e3c8a7bc99e5ff88ddfd2037a550969ab566f069ffa455df0cfae0c21f7aec3447e414eccc473a3e8b20984b90f164ac@127.0.0.1:30306"] \ No newline at end of file diff --git a/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/valid_hostname_static_nodes.json b/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/valid_hostname_static_nodes.json new file mode 100644 index 0000000000..93b882ce73 --- /dev/null +++ b/ethereum/p2p/src/test/resources/org/hyperledger/besu/ethereum/p2p/peers/valid_hostname_static_nodes.json @@ -0,0 +1,4 @@ +["enode://50203c6bfca6874370e71aecc8958529fd723feb05013dc1abca8fc1fff845c5259faba05852e9dfe5ce172a7d6e7c2a3a5eaa8b541c8af15ea5518bbff5f2fa@localhost:30303", + "enode://02beb46bc17227616be44234071dfa18516684e45eed88049190b6cb56b0bae218f045fd0450f123b8f55c60b96b78c45e8e478004293a8de6818aa4e02eff97@localhost:30304", + "enode://819e5cbd81f123516b10f04bf620daa2b385efef06d77253148b814bf1bb6197ff58ebd1fd7bf5dc765b49a4440c733bf941e479c800173f2bfeb887e4fbcbc2@127.0.0.1:30305", + "enode://6cf53e25d2a98a22e7e205a86bda7077e3c8a7bc99e5ff88ddfd2037a550969ab566f069ffa455df0cfae0c21f7aec3447e414eccc473a3e8b20984b90f164ac@127.0.0.1:30306"] \ No newline at end of file diff --git a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/AccountLocalConfigPermissioningController.java b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/AccountLocalConfigPermissioningController.java index 89a6ac2543..f9020b3545 100644 --- a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/AccountLocalConfigPermissioningController.java +++ b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/AccountLocalConfigPermissioningController.java @@ -209,6 +209,7 @@ public class AccountLocalConfigPermissioningController implements TransactionPer final LocalPermissioningConfiguration updatedConfig = PermissioningConfigurationBuilder.permissioningConfiguration( configuration.isNodeAllowlistEnabled(), + configuration.getEnodeDnsConfiguration(), configuration.getNodePermissioningConfigFilePath(), configuration.isAccountAllowlistEnabled(), configuration.getAccountPermissioningConfigFilePath()); diff --git a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfiguration.java b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfiguration.java index 27c3d001bf..410d5cddd0 100644 --- a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfiguration.java +++ b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfiguration.java @@ -14,20 +14,23 @@ */ package org.hyperledger.besu.ethereum.permissioning; -import java.net.URI; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; + import java.util.ArrayList; import java.util.Collection; import java.util.List; public class LocalPermissioningConfiguration { - private List nodeAllowlist; + private List nodeAllowlist; private List accountAllowlist; private boolean nodeAllowlistEnabled; + private EnodeDnsConfiguration enodeDnsConfiguration = EnodeDnsConfiguration.dnsDisabled(); private String nodePermissioningConfigFilePath; private boolean accountAllowlistEnabled; private String accountPermissioningConfigFilePath; - public List getNodeAllowlist() { + public List getNodeAllowlist() { return nodeAllowlist; } @@ -38,13 +41,21 @@ public class LocalPermissioningConfiguration { return config; } - public void setNodeAllowlist(final Collection nodeAllowlist) { + public void setEnodeDnsConfiguration(final EnodeDnsConfiguration enodeDnsConfiguration) { + this.enodeDnsConfiguration = enodeDnsConfiguration; + } + + public void setNodeAllowlist(final Collection nodeAllowlist) { if (nodeAllowlist != null) { this.nodeAllowlist.addAll(nodeAllowlist); this.nodeAllowlistEnabled = true; } } + public EnodeDnsConfiguration getEnodeDnsConfiguration() { + return enodeDnsConfiguration; + } + public boolean isNodeAllowlistEnabled() { return nodeAllowlistEnabled; } diff --git a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningController.java b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningController.java index 9e93380819..98b0962960 100644 --- a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningController.java +++ b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningController.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.plugin.services.metrics.Counter; import org.hyperledger.besu.util.Subscribers; import java.io.IOException; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -100,8 +99,8 @@ public class NodeLocalConfigPermissioningController implements NodePermissioning private void readNodesFromConfig(final LocalPermissioningConfiguration configuration) { if (configuration.isNodeAllowlistEnabled() && configuration.getNodeAllowlist() != null) { - for (URI uri : configuration.getNodeAllowlist()) { - addNode(EnodeURL.fromString(uri.toString())); + for (EnodeURL enodeURL : configuration.getNodeAllowlist()) { + addNode(enodeURL); } } } @@ -112,7 +111,9 @@ public class NodeLocalConfigPermissioningController implements NodePermissioning return inputValidationResult; } final List peers = - enodeURLs.stream().map(EnodeURL::fromString).collect(Collectors.toList()); + enodeURLs.stream() + .map(url -> EnodeURL.fromString(url, configuration.getEnodeDnsConfiguration())) + .collect(Collectors.toList()); for (EnodeURL peer : peers) { if (nodesAllowlist.contains(peer)) { @@ -144,7 +145,9 @@ public class NodeLocalConfigPermissioningController implements NodePermissioning return inputValidationResult; } final List peers = - enodeURLs.stream().map(EnodeURL::fromString).collect(Collectors.toList()); + enodeURLs.stream() + .map(url -> EnodeURL.fromString(url, configuration.getEnodeDnsConfiguration())) + .collect(Collectors.toList()); boolean anyBootnode = peers.stream().anyMatch(fixedNodes::contains); if (anyBootnode) { @@ -228,7 +231,7 @@ public class NodeLocalConfigPermissioningController implements NodePermissioning } public boolean isPermitted(final String enodeURL) { - return isPermitted(EnodeURL.fromString(enodeURL)); + return isPermitted(EnodeURL.fromString(enodeURL, configuration.getEnodeDnsConfiguration())); } public boolean isPermitted(final EnodeURL node) { @@ -250,6 +253,7 @@ public class NodeLocalConfigPermissioningController implements NodePermissioning final LocalPermissioningConfiguration updatedConfig = PermissioningConfigurationBuilder.permissioningConfiguration( configuration.isNodeAllowlistEnabled(), + configuration.getEnodeDnsConfiguration(), configuration.getNodePermissioningConfigFilePath(), configuration.isAccountAllowlistEnabled(), configuration.getAccountPermissioningConfigFilePath()); diff --git a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/PermissioningConfigurationBuilder.java b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/PermissioningConfigurationBuilder.java index c62a82fcd4..3276957994 100644 --- a/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/PermissioningConfigurationBuilder.java +++ b/ethereum/permissioning/src/main/java/org/hyperledger/besu/ethereum/permissioning/PermissioningConfigurationBuilder.java @@ -15,9 +15,9 @@ package org.hyperledger.besu.ethereum.permissioning; import org.hyperledger.besu.ethereum.core.Address; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; -import java.net.URI; import java.util.List; import java.util.stream.Collectors; @@ -41,6 +41,7 @@ public class PermissioningConfigurationBuilder { public static LocalPermissioningConfiguration permissioningConfiguration( final boolean nodePermissioningEnabled, + final EnodeDnsConfiguration enodeDnsConfiguration, final String nodePermissioningConfigFilepath, final boolean accountPermissioningEnabled, final String accountPermissioningConfigFilepath) @@ -48,7 +49,7 @@ public class PermissioningConfigurationBuilder { final LocalPermissioningConfiguration permissioningConfiguration = LocalPermissioningConfiguration.createDefault(); - + permissioningConfiguration.setEnodeDnsConfiguration(enodeDnsConfiguration); loadNodePermissioning( permissioningConfiguration, nodePermissioningEnabled, nodePermissioningConfigFilepath); loadAccountPermissioning( @@ -74,12 +75,15 @@ public class PermissioningConfigurationBuilder { nodePermissioningConfigFilepath); if (nodeAllowlistTomlArray != null) { - List nodesAllowlistToml = + List nodesAllowlistToml = nodeAllowlistTomlArray .toList() .parallelStream() .map(Object::toString) - .map(EnodeURL::asURI) + .map( + url -> + EnodeURL.fromString( + url, permissioningConfiguration.getEnodeDnsConfiguration())) .collect(Collectors.toList()); permissioningConfiguration.setNodeAllowlist(nodesAllowlistToml); } else { diff --git a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationBuilderTest.java b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationBuilderTest.java index a9d9992816..21dc7785f9 100644 --- a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationBuilderTest.java +++ b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationBuilderTest.java @@ -16,10 +16,14 @@ package org.hyperledger.besu.ethereum.permissioning; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; +import static org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration.dnsDisabled; + +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; +import org.hyperledger.besu.ethereum.p2p.peers.ImmutableEnodeDnsConfiguration; import java.io.IOException; -import java.net.URI; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -52,7 +56,8 @@ public class LocalPermissioningConfigurationBuilderTest { "/permissioning_config_unrecognized_key.toml"; private static final String PERMISSIONING_CONFIG_NODE_ALLOWLIST_ONLY_MULTILINE = "/permissioning_config_node_allowlist_only_multiline.toml"; - + private static final String PERMISSIONING_CONFIG_NODE_ALLOWLIST_WITH_DNS = + "/permissioning_config_node_allowlist_with_dns.toml"; private final String VALID_NODE_ID = "6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0"; @@ -71,7 +76,7 @@ public class LocalPermissioningConfigurationBuilderTest { .containsExactly("0x0000000000000000000000000000000000000009"); assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isTrue(); assertThat(permissioningConfiguration.getNodeAllowlist()) - .containsExactly(URI.create(uri), URI.create(uri2)); + .containsExactly(EnodeURL.fromString(uri), EnodeURL.fromString(uri2)); } @Test @@ -89,7 +94,7 @@ public class LocalPermissioningConfigurationBuilderTest { .containsExactly("0x0000000000000000000000000000000000000009"); assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isTrue(); assertThat(permissioningConfiguration.getNodeAllowlist()) - .containsExactly(URI.create(uri), URI.create(uri2)); + .containsExactly(EnodeURL.fromString(uri), EnodeURL.fromString(uri2)); } @Test @@ -101,11 +106,57 @@ public class LocalPermissioningConfigurationBuilderTest { LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), false, toml.toAbsolutePath().toString()); + true, + dnsDisabled(), + toml.toAbsolutePath().toString(), + false, + toml.toAbsolutePath().toString()); assertThat(permissioningConfiguration.isAccountAllowlistEnabled()).isFalse(); assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isTrue(); - assertThat(permissioningConfiguration.getNodeAllowlist()).containsExactly(URI.create(uri)); + assertThat(permissioningConfiguration.getNodeAllowlist()) + .containsExactly(EnodeURL.fromString(uri)); + } + + @Test + public void permissioningConfigWithNodeAllowlistSetWithDnsEnabled() throws Exception { + final String uri = "enode://" + VALID_NODE_ID + "@127.0.0.1:4567"; + + final URL configFile = + this.getClass().getResource(PERMISSIONING_CONFIG_NODE_ALLOWLIST_WITH_DNS); + final Path toml = createTempFile("toml", Resources.toByteArray(configFile)); + + LocalPermissioningConfiguration permissioningConfiguration = + PermissioningConfigurationBuilder.permissioningConfiguration( + true, + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build(), + toml.toAbsolutePath().toString(), + false, + toml.toAbsolutePath().toString()); + + assertThat(permissioningConfiguration.isAccountAllowlistEnabled()).isFalse(); + assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isTrue(); + assertThat(permissioningConfiguration.getNodeAllowlist()) + .containsExactly(EnodeURL.fromString(uri)); + } + + @Test + public void permissioningConfigWithNodeAllowlistSetWithDnsDisabled() throws Exception { + + final URL configFile = + this.getClass().getResource(PERMISSIONING_CONFIG_NODE_ALLOWLIST_WITH_DNS); + final Path toml = createTempFile("toml", Resources.toByteArray(configFile)); + + assertThatThrownBy( + () -> + PermissioningConfigurationBuilder.permissioningConfiguration( + true, + dnsDisabled(), + toml.toAbsolutePath().toString(), + false, + toml.toAbsolutePath().toString())) + .hasMessageContaining("Invalid enode URL syntax") + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -115,7 +166,11 @@ public class LocalPermissioningConfigurationBuilderTest { LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - false, toml.toAbsolutePath().toString(), true, toml.toAbsolutePath().toString()); + false, + dnsDisabled(), + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isFalse(); assertThat(permissioningConfiguration.isAccountAllowlistEnabled()).isTrue(); @@ -200,7 +255,7 @@ public class LocalPermissioningConfigurationBuilderTest { LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toString(), true, toml.toString()); + true, dnsDisabled(), toml.toString(), true, toml.toString()); assertThat(permissioningConfiguration.getNodePermissioningConfigFilePath()) .isEqualTo(toml.toString()); @@ -224,7 +279,7 @@ public class LocalPermissioningConfigurationBuilderTest { this.getClass().getResource(PERMISSIONING_CONFIG_NODE_ALLOWLIST_ONLY_MULTILINE); final LocalPermissioningConfiguration permissioningConfiguration = PermissioningConfigurationBuilder.permissioningConfiguration( - true, configFile.getPath(), false, configFile.getPath()); + true, dnsDisabled(), configFile.getPath(), false, configFile.getPath()); assertThat(permissioningConfiguration.isNodeAllowlistEnabled()).isTrue(); assertThat(permissioningConfiguration.getNodeAllowlist().size()).isEqualTo(5); @@ -233,18 +288,22 @@ public class LocalPermissioningConfigurationBuilderTest { private LocalPermissioningConfiguration accountOnlyPermissioningConfig(final Path toml) throws Exception { return PermissioningConfigurationBuilder.permissioningConfiguration( - false, null, true, toml.toAbsolutePath().toString()); + false, dnsDisabled(), null, true, toml.toAbsolutePath().toString()); } private LocalPermissioningConfiguration nodeOnlyPermissioningConfig(final Path toml) throws Exception { return PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), false, null); + true, dnsDisabled(), toml.toAbsolutePath().toString(), false, null); } private LocalPermissioningConfiguration permissioningConfig(final Path toml) throws Exception { return PermissioningConfigurationBuilder.permissioningConfiguration( - true, toml.toAbsolutePath().toString(), true, toml.toAbsolutePath().toString()); + true, + dnsDisabled(), + toml.toAbsolutePath().toString(), + true, + toml.toAbsolutePath().toString()); } private Path createTempFile(final String filename, final byte[] contents) throws IOException { diff --git a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationTest.java b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationTest.java index 5e63e31a07..2fbbf5a9d3 100644 --- a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationTest.java +++ b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/LocalPermissioningConfigurationTest.java @@ -16,17 +16,21 @@ package org.hyperledger.besu.ethereum.permissioning; import static org.assertj.core.api.Assertions.assertThat; -import java.net.URI; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; + import java.util.Arrays; import org.junit.Test; public class LocalPermissioningConfigurationTest { - final URI[] nodes = { - URI.create("enode://001@123:4567"), - URI.create("enode://002@123:4567"), - URI.create("enode://003@123:4567") + final EnodeURL[] nodes = { + EnodeURL.fromString( + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4567"), + EnodeURL.fromString( + "enode://7f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4568"), + EnodeURL.fromString( + "enode://8f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4569") }; @Test diff --git a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningControllerTest.java b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningControllerTest.java index c667cd6d09..ad703308ae 100644 --- a/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningControllerTest.java +++ b/ethereum/permissioning/src/test/java/org/hyperledger/besu/ethereum/permissioning/NodeLocalConfigPermissioningControllerTest.java @@ -27,7 +27,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import org.hyperledger.besu.ethereum.p2p.peers.EnodeDnsConfiguration; import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL; +import org.hyperledger.besu.ethereum.p2p.peers.ImmutableEnodeDnsConfiguration; import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController.NodesAllowlistResult; import org.hyperledger.besu.ethereum.permissioning.node.NodeAllowlistUpdatedEvent; import org.hyperledger.besu.metrics.BesuMetricCategory; @@ -35,7 +37,6 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.plugin.services.metrics.Counter; import java.io.IOException; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -66,6 +67,9 @@ public class NodeLocalConfigPermissioningControllerTest { private final EnodeURL selfEnode = EnodeURL.fromString( "enode://5f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@192.168.0.10:1111"); + private final String enodeDns = + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567"; + @Mock private MetricsSystem metricsSystem; @Mock private Counter checkCounter; @Mock private Counter checkPermittedCounter; @@ -340,7 +344,7 @@ public class NodeLocalConfigPermissioningControllerTest { .thenReturn(permissionsFile.toAbsolutePath().toString()); when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); when(permissioningConfig.getNodeAllowlist()) - .thenReturn(Arrays.asList(URI.create(expectedEnodeURL))); + .thenReturn(Arrays.asList(EnodeURL.fromString(expectedEnodeURL))); controller = new NodeLocalConfigPermissioningController( permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); @@ -360,7 +364,7 @@ public class NodeLocalConfigPermissioningControllerTest { when(permissioningConfig.getNodePermissioningConfigFilePath()).thenReturn("foo"); when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); when(permissioningConfig.getNodeAllowlist()) - .thenReturn(Arrays.asList(URI.create(expectedEnodeURI))); + .thenReturn(Arrays.asList(EnodeURL.fromString(expectedEnodeURI))); controller = new NodeLocalConfigPermissioningController( permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); @@ -459,7 +463,8 @@ public class NodeLocalConfigPermissioningControllerTest { when(permissioningConfig.getNodePermissioningConfigFilePath()) .thenReturn(permissionsFile.toAbsolutePath().toString()); when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); - when(permissioningConfig.getNodeAllowlist()).thenReturn(Arrays.asList(URI.create(enode1))); + when(permissioningConfig.getNodeAllowlist()) + .thenReturn(Arrays.asList(EnodeURL.fromString(enode1))); controller = new NodeLocalConfigPermissioningController( permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); @@ -482,7 +487,8 @@ public class NodeLocalConfigPermissioningControllerTest { when(permissioningConfig.getNodePermissioningConfigFilePath()) .thenReturn(permissionsFile.toAbsolutePath().toString()); when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); - when(permissioningConfig.getNodeAllowlist()).thenReturn(Arrays.asList(URI.create(enode1))); + when(permissioningConfig.getNodeAllowlist()) + .thenReturn(Arrays.asList(EnodeURL.fromString(enode1))); controller = new NodeLocalConfigPermissioningController( permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); @@ -493,6 +499,52 @@ public class NodeLocalConfigPermissioningControllerTest { verifyZeroInteractions(consumer); } + @Test + @SuppressWarnings("unchecked") + public void getNodeAllowlistShouldWorkWhenOnlyDnsEnabled() throws Exception { + final Path permissionsFile = createPermissionsFileWithNode(enodeDns); + final LocalPermissioningConfiguration permissioningConfig = + mock(LocalPermissioningConfiguration.class); + + final EnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(false).build(); + + when(permissioningConfig.getNodePermissioningConfigFilePath()) + .thenReturn(permissionsFile.toAbsolutePath().toString()); + when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); + when(permissioningConfig.getNodeAllowlist()) + .thenReturn(singletonList(EnodeURL.fromString(enodeDns, enodeDnsConfiguration))); + controller = + new NodeLocalConfigPermissioningController( + permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); + assertThat(controller.getNodesAllowlist()).hasSize(1); + assertThat(controller.getNodesAllowlist()) + .contains( + "enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@127.0.0.1:4567"); + } + + @Test + @SuppressWarnings("unchecked") + public void getNodeAllowlistShouldWorkWhenDnsAndUpdateEnabled() throws Exception { + final Path permissionsFile = createPermissionsFileWithNode(enodeDns); + final LocalPermissioningConfiguration permissioningConfig = + mock(LocalPermissioningConfiguration.class); + + final EnodeDnsConfiguration enodeDnsConfiguration = + ImmutableEnodeDnsConfiguration.builder().dnsEnabled(true).updateEnabled(true).build(); + + when(permissioningConfig.getNodePermissioningConfigFilePath()) + .thenReturn(permissionsFile.toAbsolutePath().toString()); + when(permissioningConfig.isNodeAllowlistEnabled()).thenReturn(true); + when(permissioningConfig.getNodeAllowlist()) + .thenReturn(singletonList(EnodeURL.fromString(enodeDns, enodeDnsConfiguration))); + controller = + new NodeLocalConfigPermissioningController( + permissioningConfig, bootnodesList, selfEnode.getNodeId(), metricsSystem); + assertThat(controller.getNodesAllowlist()).hasSize(1); + assertThat(controller.getNodesAllowlist()).contains(enodeDns); + } + private Path createPermissionsFileWithNode(final String node) throws IOException { final String nodePermissionsFileContent = "nodes-allowlist=[\"" + node + "\"]"; final Path permissionsFile = Files.createTempFile("node_permissions", ""); diff --git a/ethereum/permissioning/src/test/resources/permissioning_config_node_allowlist_with_dns.toml b/ethereum/permissioning/src/test/resources/permissioning_config_node_allowlist_with_dns.toml new file mode 100644 index 0000000000..a76886f921 --- /dev/null +++ b/ethereum/permissioning/src/test/resources/permissioning_config_node_allowlist_with_dns.toml @@ -0,0 +1,3 @@ +# Permissioning TOML file (node allowlist with dns only) + +nodes-allowlist=["enode://6f8a80d14311c39f35f516fa664deaaaa13e85b2f7493f37f6144d86991ec012937307647bd3b9a82abe2974e1407241d54947bbb39763a4cac9f77166ad92a0@localhost:4567"]