[BOUNTY-2] Explicit Configuration & General internal NAT API #190 (#298)

* refactor nat manager

Signed-off-by: Karim TAAM <karim.t2am@gmail.com>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* restart tests

Signed-off-by: Karim TAAM <karim.t2am@gmail.com>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* restart tests

Signed-off-by: Karim TAAM <karim.t2am@gmail.com>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Add Tuweni to Plugin-APIs (#295)

Generally, byte[] -> Bytes of some form.  Most of the changes are the
side effect of the type changes or chaning to the names of Tuweni
equivilant calls (getHexString->toHexString, etc).

UnformattedData -> Bytes
Log Topics went from Hash to Bytes32
Difficulty went to UInt256 to match core impl.
Quantity lost BinaryData and is just getValue() and toHexString()

Signed-off-by: Danno Ferrin <danno.ferrin@gmail.com>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* DSL precondition to avoid creating orphan processes (#291)

Signed-off-by: Christopher Hare <chris.hare@consensys.net>

Co-authored-by: Danno Ferrin <danno.ferrin@shemnon.com>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Fix AT failure because of additional field sent by Orion (#299)

Signed-off-by: Stefan Pingel <stefan.pingel@consensys.net>
Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Update NatService.java

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Update NatServiceType.java

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Update NatServiceType.java

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Update NatServiceType.java

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Update NatServiceType.java

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* remove method

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* SPDX-License-Identifier: Apache-2.0

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* SPDX-License-Identifier: Apache-2.0

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* Comment waitForFile: "besu.networks"

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* spotless apply

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* test

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* create file if not exist

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* remove useless code

Signed-off-by: Abdelhamid Bakhta <abdelhamid.bakhta@consensys.net>

* fix waitForFile method

Signed-off-by: Karim TAAM <karim.t2am@gmail.com>

Co-authored-by: Danno Ferrin <danno.ferrin@shemnon.com>
Co-authored-by: CJ Hare <CjHare@users.noreply.github.com>
Co-authored-by: pinges <16143240+pinges@users.noreply.github.com>
Co-authored-by: Abdelhamid Bakhta <45264458+abdelhamidbakhta@users.noreply.github.com>
pull/301/head
Karim T 5 years ago committed by Abdelhamid Bakhta
parent ddd8aae08f
commit 624c25ec7e
  1. 13
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/BesuNodeRunner.java
  2. 3
      acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java
  3. 59
      besu/src/main/java/org/hyperledger/besu/Runner.java
  4. 47
      besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java
  5. 2
      besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java
  6. 9
      besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java
  7. 0
      config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java
  8. 3
      ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcTestMethodsFactory.java
  9. 32
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpService.java
  10. 56
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AdminNodeInfo.java
  11. 13
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/AdminJsonRpcMethods.java
  12. 5
      ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/JsonRpcMethodsFactory.java
  13. 7
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/AbstractJsonRpcHttpServiceTest.java
  14. 4
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceCorsTest.java
  15. 6
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceHostWhitelistTest.java
  16. 6
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceLoginTest.java
  17. 17
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceRpcApisTest.java
  18. 7
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/JsonRpcHttpServiceTest.java
  19. 59
      ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AdminNodeInfoTest.java
  20. 27
      ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryAgent.java
  21. 7
      ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/discovery/VertxPeerDiscoveryAgent.java
  22. 85
      ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetwork.java
  23. 5
      ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/PeerDiscoveryTestHelper.java
  24. 7
      ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/discovery/internal/MockPeerDiscoveryAgent.java
  25. 24
      ethereum/p2p/src/test/java/org/hyperledger/besu/ethereum/p2p/network/DefaultP2PNetworkTest.java
  26. 5
      ethereum/retesteth/src/main/java/org/hyperledger/besu/ethereum/retesteth/RetestethService.java
  27. 2
      nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java
  28. 204
      nat/src/main/java/org/hyperledger/besu/nat/NatService.java
  29. 127
      nat/src/main/java/org/hyperledger/besu/nat/core/AbstractNatManager.java
  30. 37
      nat/src/main/java/org/hyperledger/besu/nat/core/AutoDetectionResult.java
  31. 83
      nat/src/main/java/org/hyperledger/besu/nat/core/NatManager.java
  32. 22
      nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodAutoDetection.java
  33. 73
      nat/src/main/java/org/hyperledger/besu/nat/core/domain/NatPortMapping.java
  34. 59
      nat/src/main/java/org/hyperledger/besu/nat/core/domain/NatServiceType.java
  35. 29
      nat/src/main/java/org/hyperledger/besu/nat/core/domain/NetworkProtocol.java
  36. 99
      nat/src/main/java/org/hyperledger/besu/nat/manual/ManualNatManager.java
  37. 111
      nat/src/main/java/org/hyperledger/besu/nat/upnp/UpnpNatManager.java
  38. 172
      nat/src/test/java/org/hyperledger/besu/nat/NatServiceTest.java
  39. 109
      nat/src/test/java/org/hyperledger/besu/nat/core/AbstractNatManagerTest.java
  40. 117
      nat/src/test/java/org/hyperledger/besu/nat/manual/ManualNatManagerTest.java
  41. 8
      nat/src/test/java/org/hyperledger/besu/nat/upnp/UpnpNatManagerTest.java

@ -16,6 +16,7 @@ package org.hyperledger.besu.tests.acceptance.dsl.node;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@ -40,16 +41,14 @@ public interface BesuNodeRunner {
boolean isActive(String nodeName);
default void waitForPortsFile(final Path dataDir) {
final File file = new File(dataDir.toFile(), "besu.ports");
default void waitForFile(final Path dataDir, final String fileName) {
final File file = new File(dataDir.toFile(), fileName);
Awaitility.waitAtMost(30, TimeUnit.SECONDS)
.until(
() -> {
if (file.exists()) {
try (final Stream<String> s = Files.lines(file.toPath())) {
return s.count() > 0;
}
} else {
try (final Stream<String> s = Files.lines(file.toPath())) {
return s.count() > 0;
} catch (NoSuchFileException __) {
return false;
}
});

@ -277,7 +277,8 @@ public class ProcessBesuNodeRunner implements BesuNodeRunner {
LOG.error("Error starting BesuNode process", e);
}
waitForPortsFile(dataDir);
waitForFile(dataDir, "besu.ports");
waitForFile(dataDir, "besu.networks");
}
private boolean isNotAliveOrphan(final String name) {

@ -22,7 +22,7 @@ import org.hyperledger.besu.ethereum.p2p.network.NetworkRunner;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.stratum.StratumServer;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.nat.NatService;
import java.io.File;
import java.io.FileOutputStream;
@ -49,7 +49,7 @@ public class Runner implements AutoCloseable {
private final CountDownLatch shutdown = new CountDownLatch(1);
private final NetworkRunner networkRunner;
private final Optional<UpnpNatManager> natManager;
private final NatService natService;
private final Optional<JsonRpcHttpService> jsonRpc;
private final Optional<GraphQLHttpService> graphQLHttp;
private final Optional<WebSocketService> websocketRpc;
@ -62,7 +62,7 @@ public class Runner implements AutoCloseable {
Runner(
final Vertx vertx,
final NetworkRunner networkRunner,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final Optional<JsonRpcHttpService> jsonRpc,
final Optional<GraphQLHttpService> graphQLHttp,
final Optional<WebSocketService> websocketRpc,
@ -72,7 +72,7 @@ public class Runner implements AutoCloseable {
final Path dataDir) {
this.vertx = vertx;
this.networkRunner = networkRunner;
this.natManager = natManager;
this.natService = natService;
this.graphQLHttp = graphQLHttp;
this.jsonRpc = jsonRpc;
this.websocketRpc = websocketRpc;
@ -85,7 +85,7 @@ public class Runner implements AutoCloseable {
public void start() {
try {
LOG.info("Starting Ethereum main loop ... ");
natManager.ifPresent(UpnpNatManager::start);
natService.start();
networkRunner.start();
if (networkRunner.getNetwork().isP2pEnabled()) {
besuController.getSynchronizer().start();
@ -102,6 +102,7 @@ public class Runner implements AutoCloseable {
metrics.ifPresent(service -> waitForServiceToStart("metrics", service.start()));
LOG.info("Ethereum main loop is up.");
writeBesuPortsToFile();
writeBesuNetworksToFile();
} catch (final Exception ex) {
LOG.error("Startup failed", ex);
throw new IllegalStateException(ex);
@ -125,7 +126,7 @@ public class Runner implements AutoCloseable {
networkRunner.stop();
waitForServiceToStop("Network", networkRunner::awaitStop);
natManager.ifPresent(UpnpNatManager::stop);
natService.stop();
besuController.close();
vertx.close((res) -> vertxShutdownLatch.countDown());
waitForServiceToStop("Vertx", vertxShutdownLatch::await);
@ -218,17 +219,33 @@ public class Runner implements AutoCloseable {
if (getMetricsPort().isPresent()) {
properties.setProperty("metrics", String.valueOf(getMetricsPort().get()));
}
// create besu.ports file
createBesuFile(
properties, "ports", "This file contains the ports used by the running instance of Besu");
}
final File portsFile = new File(dataDir.toFile(), "besu.ports");
portsFile.deleteOnExit();
private void writeBesuNetworksToFile() {
final Properties properties = new Properties();
if (networkRunner.getNetwork().isP2pEnabled()) {
networkRunner
.getNetwork()
.getLocalEnode()
.ifPresent(
enode -> {
final String globalIp =
natService.queryExternalIPAddress().orElseGet(enode::getIpAsString);
properties.setProperty("global-ip", globalIp);
try (final FileOutputStream fileOutputStream = new FileOutputStream(portsFile)) {
properties.store(
fileOutputStream,
"This file contains the ports used by the running instance of Besu. This file will be deleted after the node is shutdown.");
} catch (final Exception e) {
LOG.warn("Error writing ports file", e);
final String localIp =
natService.queryLocalIPAddress().orElseGet(enode::getIpAsString);
properties.setProperty("local-ip", localIp);
});
}
// create besu.networks file
createBesuFile(
properties,
"networks",
"This file contains the IP Addresses (global and local) used by the running instance of Besu");
}
public Optional<Integer> getJsonRpcPort() {
@ -260,4 +277,18 @@ public class Runner implements AutoCloseable {
private interface SynchronousShutdown {
void await() throws InterruptedException;
}
private void createBesuFile(
final Properties properties, final String fileName, final String fileHeader) {
final File file = new File(dataDir.toFile(), String.format("besu.%s", fileName));
file.deleteOnExit();
try (final FileOutputStream fileOutputStream = new FileOutputStream(file)) {
properties.store(
fileOutputStream,
String.format("%s. This file will be deleted after the node is shutdown.", fileHeader));
} catch (final Exception e) {
LOG.warn(String.format("Error writing %s file", fileName), e);
}
}
}

@ -16,6 +16,8 @@ package org.hyperledger.besu;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.isNull;
import static java.util.function.Predicate.isEqual;
import static java.util.function.Predicate.not;
import static org.hyperledger.besu.controller.BesuController.CACHE_PATH;
import org.hyperledger.besu.cli.config.EthNetworkConfig;
@ -87,6 +89,9 @@ import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.metrics.prometheus.MetricsService;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.NatManager;
import org.hyperledger.besu.nat.manual.ManualNatManager;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.services.BesuPluginContextImpl;
@ -121,7 +126,7 @@ public class RunnerBuilder {
private String p2pAdvertisedHost;
private String p2pListenInterface = NetworkUtility.INADDR_ANY;
private int p2pListenPort;
private NatMethod natMethod = NatMethod.NONE;
private NatMethod natMethod = NatMethod.AUTO;
private int maxPeers;
private boolean limitRemoteWireConnectionsEnabled = false;
private float fractionRemoteConnectionsAllowed;
@ -333,7 +338,7 @@ public class RunnerBuilder {
.map(nodePerms -> PeerPermissions.combine(nodePerms, bannedNodes))
.orElse(bannedNodes);
final Optional<UpnpNatManager> natManager = buildNatManager(natMethod);
final NatService natService = new NatService(buildNatManager(natMethod));
final NetworkBuilder inactiveNetwork = (caps) -> new NoopP2PNetwork();
final NetworkBuilder activeNetwork =
@ -345,7 +350,7 @@ public class RunnerBuilder {
.peerPermissions(peerPermissions)
.metricsSystem(metricsSystem)
.supportedCapabilities(caps)
.natManager(natManager)
.natService(natService)
.build();
final NetworkRunner networkRunner =
@ -428,6 +433,7 @@ public class RunnerBuilder {
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration,
natService,
besuPluginContext.getNamedPlugins());
jsonRpcHttpService =
Optional.of(
@ -436,7 +442,7 @@ public class RunnerBuilder {
dataDir,
jsonRpcConfiguration,
metricsSystem,
natManager,
natService,
jsonRpcMethods,
new HealthService(new LivenessCheck()),
new HealthService(new ReadinessCheck(peerNetwork, synchronizer))));
@ -486,6 +492,7 @@ public class RunnerBuilder {
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration,
natService,
besuPluginContext.getNamedPlugins());
final SubscriptionManager subscriptionManager =
@ -512,7 +519,7 @@ public class RunnerBuilder {
return new Runner(
vertx,
networkRunner,
natManager,
natService,
jsonRpcHttpService,
graphQLHttpService,
webSocketService,
@ -547,16 +554,6 @@ public class RunnerBuilder {
}
}
private Optional<UpnpNatManager> buildNatManager(final NatMethod natMethod) {
switch (natMethod) {
case UPNP:
return Optional.of(new UpnpNatManager());
case NONE:
default:
return Optional.ofNullable(null);
}
}
private Optional<AccountPermissioningController> buildAccountPermissioningController(
final Optional<PermissioningConfiguration> permissioningConfiguration,
final BesuController<?> besuController,
@ -579,6 +576,24 @@ public class RunnerBuilder {
}
}
private Optional<NatManager> buildNatManager(final NatMethod natMethod) {
final NatMethod detectedNatMethod =
Optional.of(natMethod)
.filter(not(isEqual(NatMethod.AUTO)))
.orElse(NatService.autoDetectNatMethod());
switch (detectedNatMethod) {
case UPNP:
return Optional.of(new UpnpNatManager());
case MANUAL:
return Optional.of(
new ManualNatManager(p2pAdvertisedHost, p2pListenPort, jsonRpcConfiguration.getPort()));
case NONE:
default:
return Optional.empty();
}
}
@VisibleForTesting
public static Collection<EnodeURL> getFixedNodes(
final Collection<EnodeURL> someFixedNodes, final Collection<EnodeURL> moreFixedNodes) {
@ -616,6 +631,7 @@ public class RunnerBuilder {
final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final NatService natService,
final Map<String, BesuPlugin> namedPlugins) {
final Map<String, JsonRpcMethod> methods =
new JsonRpcMethodsFactory()
@ -639,6 +655,7 @@ public class RunnerBuilder {
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration,
natService,
namedPlugins);
methods.putAll(besuController.getAdditionalJsonRpcMethods(jsonRpcApis));
return methods;

@ -64,7 +64,7 @@ public interface DefaultCommandValues {
String MANDATORY_HOST_FORMAT_HELP = "<HOST>";
String MANDATORY_PORT_FORMAT_HELP = "<PORT>";
SyncMode DEFAULT_SYNC_MODE = SyncMode.FULL;
NatMethod DEFAULT_NAT_METHOD = NatMethod.NONE;
NatMethod DEFAULT_NAT_METHOD = NatMethod.AUTO;
int FAST_SYNC_MIN_PEER_COUNT = 5;
int DEFAULT_MAX_PEERS = 25;
float DEFAULT_FRACTION_REMOTE_WIRE_CONNECTIONS_ALLOWED =

@ -1388,6 +1388,9 @@ public class BesuCommandTest extends CommandTestAbstract {
parseCommand("--nat-method", "UPNP");
verify(mockRunnerBuilder).natMethod(eq(NatMethod.UPNP));
parseCommand("--nat-method", "AUTO");
verify(mockRunnerBuilder).natMethod(eq(NatMethod.AUTO));
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();
}
@ -1400,7 +1403,7 @@ public class BesuCommandTest extends CommandTestAbstract {
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString())
.contains(
"Invalid value for option '--nat-method': expected one of [UPNP, NONE] (case-insensitive) but was 'invalid'");
"Invalid value for option '--nat-method': expected one of [UPNP, MANUAL, AUTO, NONE] (case-insensitive) but was 'invalid'");
}
@Test
@ -1414,10 +1417,10 @@ public class BesuCommandTest extends CommandTestAbstract {
}
@Test
public void natMethodPropertyDefaultIsNone() {
public void natMethodPropertyDefaultIsAuto() {
parseCommand();
verify(mockRunnerBuilder).natMethod(eq(NatMethod.NONE));
verify(mockRunnerBuilder).natMethod(eq(NatMethod.AUTO));
assertThat(commandOutput.toString()).isEmpty();
assertThat(commandErrorOutput.toString()).isEmpty();

@ -45,6 +45,7 @@ import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.math.BigInteger;
import java.util.ArrayList;
@ -100,6 +101,7 @@ public class JsonRpcTestMethodsFactory {
final JsonRpcConfiguration jsonRpcConfiguration = mock(JsonRpcConfiguration.class);
final WebSocketConfiguration webSocketConfiguration = mock(WebSocketConfiguration.class);
final MetricsConfiguration metricsConfiguration = mock(MetricsConfiguration.class);
final NatService natService = new NatService(Optional.empty());
final List<RpcApi> apis = new ArrayList<>();
apis.add(RpcApis.ETH);
@ -127,6 +129,7 @@ public class JsonRpcTestMethodsFactory {
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration,
natService,
new HashMap<>());
}
}

@ -33,6 +33,10 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcRespon
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponseType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcUnauthorizedResponse;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.plugin.services.metrics.LabelledMetric;
@ -84,7 +88,7 @@ public class JsonRpcHttpService {
private final Vertx vertx;
private final JsonRpcConfiguration config;
private final Map<String, JsonRpcMethod> rpcMethods;
private final Optional<UpnpNatManager> natManager;
private final NatService natService;
private final Path dataDir;
private final LabelledMetric<OperationTimer> requestTimer;
@ -101,7 +105,7 @@ public class JsonRpcHttpService {
* @param dataDir The data directory where requests can be buffered
* @param config Configuration for the rpc methods being loaded
* @param metricsSystem The metrics service that activities should be reported to
* @param natManager The NAT environment manager.
* @param natService The NAT environment manager.
* @param methods The json rpc methods that should be enabled
* @param livenessService A service responsible for reporting whether this node is live
* @param readinessService A service responsible for reporting whether this node has fully started
@ -111,7 +115,7 @@ public class JsonRpcHttpService {
final Path dataDir,
final JsonRpcConfiguration config,
final MetricsSystem metricsSystem,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final Map<String, JsonRpcMethod> methods,
final HealthService livenessService,
final HealthService readinessService) {
@ -120,7 +124,7 @@ public class JsonRpcHttpService {
dataDir,
config,
metricsSystem,
natManager,
natService,
methods,
AuthenticationService.create(vertx, config),
livenessService,
@ -132,7 +136,7 @@ public class JsonRpcHttpService {
final Path dataDir,
final JsonRpcConfiguration config,
final MetricsSystem metricsSystem,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final Map<String, JsonRpcMethod> methods,
final Optional<AuthenticationService> authenticationService,
final HealthService livenessService,
@ -147,7 +151,7 @@ public class JsonRpcHttpService {
validateConfig(config);
this.config = config;
this.vertx = vertx;
this.natManager = natManager;
this.natService = natService;
this.rpcMethods = methods;
this.authenticationService = authenticationService;
this.livenessService = livenessService;
@ -230,13 +234,15 @@ public class JsonRpcHttpService {
LOG.info(
"JsonRPC service started and listening on {}:{}", config.getHost(), actualPort);
config.setPort(actualPort);
// Request that a NAT port forward for our server port
if (natManager.isPresent()) {
natManager
.get()
.requestPortForward(
config.getPort(), UpnpNatManager.Protocol.TCP, "besu-json-rpc");
}
natService.ifNatEnvironment(
NatMethod.UPNP,
natManager -> {
((UpnpNatManager) natManager)
.requestPortForward(
config.getPort(), NetworkProtocol.TCP, NatServiceType.JSON_RPC);
});
return;
}
httpServer = null;

@ -25,8 +25,13 @@ import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.chain.ChainHead;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.math.BigInteger;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@ -41,18 +46,21 @@ public class AdminNodeInfo implements JsonRpcMethod {
private final GenesisConfigOptions genesisConfigOptions;
private final P2PNetwork peerNetwork;
private final BlockchainQueries blockchainQueries;
private final NatService natService;
public AdminNodeInfo(
final String clientVersion,
final BigInteger networkId,
final GenesisConfigOptions genesisConfigOptions,
final P2PNetwork peerNetwork,
final BlockchainQueries blockchainQueries) {
final BlockchainQueries blockchainQueries,
final NatService natService) {
this.peerNetwork = peerNetwork;
this.clientVersion = clientVersion;
this.genesisConfigOptions = genesisConfigOptions;
this.blockchainQueries = blockchainQueries;
this.networkId = networkId;
this.natService = natService;
}
@Override
@ -76,20 +84,28 @@ public class AdminNodeInfo implements JsonRpcMethod {
}
final EnodeURL enode = maybeEnode.get();
final Bytes nodeId = enode.getNodeId();
response.put("enode", enode.toString());
response.put("ip", enode.getIpAsString());
final Bytes nodeId = enode.getNodeId();
final String ip = getIp(enode);
final int listeningPort = getListeningPort(enode);
final int discoveryPort = getDiscoveryPort(enode);
response.put("enode", getNodeAsString(enode, ip, listeningPort, discoveryPort));
response.put("ip", ip);
if (enode.isListening()) {
response.put("listenAddr", enode.getIpAsString() + ":" + enode.getListeningPort().getAsInt());
response.put("listenAddr", String.format("%s:%d", ip, listeningPort));
}
response.put("id", nodeId.toUnprefixedHexString());
response.put("name", clientVersion);
if (enode.isRunningDiscovery()) {
ports.put("discovery", enode.getDiscoveryPortOrZero());
ports.put("discovery", discoveryPort);
}
if (enode.isListening()) {
ports.put("listener", enode.getListeningPort().getAsInt());
ports.put("listener", listeningPort);
}
response.put("ports", ports);
@ -112,4 +128,34 @@ public class AdminNodeInfo implements JsonRpcMethod {
return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), response);
}
private String getNodeAsString(
final EnodeURL enodeURL, final String ip, final int listeningPort, final int discoveryPort) {
final String uri =
String.format(
"enode://%s@%s:%d", enodeURL.getNodeId().toUnprefixedHexString(), ip, listeningPort);
if (listeningPort != discoveryPort) {
return URI.create(uri + String.format("?discport=%d", discoveryPort)).toString();
} else {
return URI.create(uri).toString();
}
}
private String getIp(final EnodeURL enode) {
return natService.queryExternalIPAddress().orElseGet(enode::getIpAsString);
}
private int getDiscoveryPort(final EnodeURL enode) {
return natService
.getPortMapping(NatServiceType.DISCOVERY, NetworkProtocol.UDP)
.map(NatPortMapping::getExternalPort)
.orElseGet(enode::getDiscoveryPortOrZero);
}
private int getListeningPort(final EnodeURL enode) {
return natService
.getPortMapping(NatServiceType.RLPX, NetworkProtocol.TCP)
.map(NatPortMapping::getExternalPort)
.orElseGet(enode::getListeningPortOrZero);
}
}

@ -27,6 +27,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.JsonRpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.PluginsReloadConfiguration;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.plugin.BesuPlugin;
import java.math.BigInteger;
@ -39,6 +40,7 @@ public class AdminJsonRpcMethods extends ApiGroupJsonRpcMethods {
private final GenesisConfigOptions genesisConfigOptions;
private final P2PNetwork p2pNetwork;
private final BlockchainQueries blockchainQueries;
private final NatService natService;
private final Map<String, BesuPlugin> namedPlugins;
public AdminJsonRpcMethods(
@ -47,13 +49,15 @@ public class AdminJsonRpcMethods extends ApiGroupJsonRpcMethods {
final GenesisConfigOptions genesisConfigOptions,
final P2PNetwork p2pNetwork,
final BlockchainQueries blockchainQueries,
final Map<String, BesuPlugin> namedPlugins) {
final Map<String, BesuPlugin> namedPlugins,
final NatService natService) {
this.clientVersion = clientVersion;
this.networkId = networkId;
this.genesisConfigOptions = genesisConfigOptions;
this.p2pNetwork = p2pNetwork;
this.blockchainQueries = blockchainQueries;
this.namedPlugins = namedPlugins;
this.natService = natService;
}
@Override
@ -67,7 +71,12 @@ public class AdminJsonRpcMethods extends ApiGroupJsonRpcMethods {
new AdminAddPeer(p2pNetwork),
new AdminRemovePeer(p2pNetwork),
new AdminNodeInfo(
clientVersion, networkId, genesisConfigOptions, p2pNetwork, blockchainQueries),
clientVersion,
networkId,
genesisConfigOptions,
p2pNetwork,
blockchainQueries,
natService),
new AdminPeers(p2pNetwork),
new AdminChangeLogLevel(),
new AdminGenerateLogBloomCache(blockchainQueries),

@ -33,6 +33,7 @@ import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioni
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.plugin.BesuPlugin;
import java.math.BigInteger;
@ -65,6 +66,7 @@ public class JsonRpcMethodsFactory {
final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration,
final MetricsConfiguration metricsConfiguration,
final NatService natService,
final Map<String, BesuPlugin> namedPlugins) {
final Map<String, JsonRpcMethod> enabled = new HashMap<>();
@ -80,7 +82,8 @@ public class JsonRpcMethodsFactory {
genesisConfigOptions,
p2pNetwork,
blockchainQueries,
namedPlugins),
namedPlugins,
natService),
new DebugJsonRpcMethods(blockchainQueries, protocolSchedule, metricsSystem),
new EeaJsonRpcMethods(
blockchainQueries, protocolSchedule, transactionPool, privacyParameters),

@ -44,6 +44,7 @@ import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.testutil.BlockTestUtil.ChainResources;
import java.math.BigInteger;
@ -139,6 +140,8 @@ public abstract class AbstractJsonRpcHttpServiceTest {
supportedCapabilities.add(EthProtocol.ETH62);
supportedCapabilities.add(EthProtocol.ETH63);
final NatService natService = new NatService(Optional.empty());
return new JsonRpcMethodsFactory()
.methods(
CLIENT_VERSION,
@ -160,6 +163,7 @@ public abstract class AbstractJsonRpcHttpServiceTest {
config,
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>());
}
@ -171,6 +175,7 @@ public abstract class AbstractJsonRpcHttpServiceTest {
final JsonRpcConfiguration config = JsonRpcConfiguration.createDefault();
final Map<String, JsonRpcMethod> methods = getRpcMethods(config, blockchainSetupUtil);
final NatService natService = new NatService(Optional.empty());
config.setPort(0);
service =
@ -179,7 +184,7 @@ public abstract class AbstractJsonRpcHttpServiceTest {
folder.newFolder().toPath(),
config,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
methods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);

@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.ethereum.api.jsonrpc.health.HealthService;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.nat.NatService;
import java.util.HashMap;
import java.util.Optional;
@ -189,6 +190,7 @@ public class JsonRpcHttpServiceCorsTest {
if (corsAllowedDomains != null) {
config.setCorsAllowedDomains(Lists.newArrayList(corsAllowedDomains));
}
final NatService natService = new NatService(Optional.empty());
final JsonRpcHttpService jsonRpcHttpService =
new JsonRpcHttpService(
@ -196,7 +198,7 @@ public class JsonRpcHttpServiceCorsTest {
folder.newFolder().toPath(),
config,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
new HashMap<>(),
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);

@ -37,6 +37,7 @@ import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioni
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.io.IOException;
import java.math.BigInteger;
@ -78,6 +79,8 @@ public class JsonRpcHttpServiceHostWhitelistTest {
private static final Collection<RpcApi> JSON_RPC_APIS =
Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3);
private final JsonRpcConfiguration jsonRpcConfig = createJsonRpcConfig();
private final NatService natService = new NatService(Optional.empty());
private final List<String> hostsWhitelist = Arrays.asList("ally", "friend");
@Before
@ -114,6 +117,7 @@ public class JsonRpcHttpServiceHostWhitelistTest {
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>()));
service = createJsonRpcHttpService();
service.start().join();
@ -128,7 +132,7 @@ public class JsonRpcHttpServiceHostWhitelistTest {
folder.newFolder().toPath(),
jsonRpcConfig,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);

@ -44,6 +44,7 @@ import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.io.IOException;
import java.math.BigInteger;
@ -104,6 +105,7 @@ public class JsonRpcHttpServiceLoginTest {
protected static JWTAuth jwtAuth;
protected static String authPermissionsConfigFilePath = "JsonRpcHttpService/auth.toml";
protected final JsonRpcTestHelper testHelper = new JsonRpcTestHelper();
protected static final NatService natService = new NatService(Optional.empty());
@BeforeClass
public static void initServerAndClient() throws Exception {
@ -117,6 +119,7 @@ public class JsonRpcHttpServiceLoginTest {
final StubGenesisConfigOptions genesisConfigOptions =
new StubGenesisConfigOptions().constantinopleBlock(0).chainId(CHAIN_ID);
rpcMethods =
spy(
new JsonRpcMethodsFactory()
@ -140,6 +143,7 @@ public class JsonRpcHttpServiceLoginTest {
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>()));
service = createJsonRpcHttpService();
jwtAuth = service.authenticationService.get().getJwtAuthProvider();
@ -165,7 +169,7 @@ public class JsonRpcHttpServiceLoginTest {
folder.newFolder().toPath(),
config,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);

@ -44,6 +44,7 @@ import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioni
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import java.math.BigInteger;
import java.util.ArrayList;
@ -91,6 +92,7 @@ public class JsonRpcHttpServiceRpcApisTest {
@Mock protected static BlockchainQueries blockchainQueries;
private final JsonRpcTestHelper testHelper = new JsonRpcTestHelper();
private final NatService natService = new NatService(Optional.empty());
@Before
public void before() {
@ -208,6 +210,7 @@ public class JsonRpcHttpServiceRpcApisTest {
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>()));
final JsonRpcHttpService jsonRpcHttpService =
new JsonRpcHttpService(
@ -215,7 +218,7 @@ public class JsonRpcHttpServiceRpcApisTest {
folder.newFolder().toPath(),
config,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);
@ -268,7 +271,8 @@ public class JsonRpcHttpServiceRpcApisTest {
final JsonRpcConfiguration jsonRpcConfiguration,
final WebSocketConfiguration webSocketConfiguration,
final P2PNetwork p2pNetwork,
final MetricsConfiguration metricsConfiguration)
final MetricsConfiguration metricsConfiguration,
final NatService natService)
throws Exception {
final Set<Capability> supportedCapabilities = new HashSet<>();
supportedCapabilities.add(EthProtocol.ETH62);
@ -299,6 +303,7 @@ public class JsonRpcHttpServiceRpcApisTest {
jsonRpcConfiguration,
webSocketConfiguration,
metricsConfiguration,
natService,
new HashMap<>()));
final JsonRpcHttpService jsonRpcHttpService =
new JsonRpcHttpService(
@ -306,7 +311,7 @@ public class JsonRpcHttpServiceRpcApisTest {
folder.newFolder().toPath(),
jsonRpcConfiguration,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);
@ -390,6 +395,7 @@ public class JsonRpcHttpServiceRpcApisTest {
WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault();
P2PNetwork p2pNetwork = mock(P2PNetwork.class);
MetricsConfiguration metricsConfiguration = MetricsConfiguration.builder().build();
NatService natService = mock(NatService.class);
if (enabledNetServices[netServices.indexOf("jsonrpc")]) {
jsonRpcConfiguration = createJsonRpcConfiguration();
@ -406,7 +412,7 @@ public class JsonRpcHttpServiceRpcApisTest {
}
return createJsonRpcHttpService(
jsonRpcConfiguration, webSocketConfiguration, p2pNetwork, metricsConfiguration);
jsonRpcConfiguration, webSocketConfiguration, p2pNetwork, metricsConfiguration, natService);
}
@Test
@ -417,7 +423,8 @@ public class JsonRpcHttpServiceRpcApisTest {
JsonRpcConfiguration.createDefault(),
WebSocketConfiguration.createDefault(),
mock(P2PNetwork.class),
MetricsConfiguration.builder().build());
MetricsConfiguration.builder().build(),
natService);
final RequestBody body = createNetServicesRequestBody();
try (final Response resp = client.newCall(buildRequest(body)).execute()) {

@ -55,6 +55,7 @@ import org.hyperledger.besu.ethereum.permissioning.AccountLocalConfigPermissioni
import org.hyperledger.besu.ethereum.permissioning.NodeLocalConfigPermissioningController;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.plugin.data.SyncStatus;
import java.math.BigInteger;
@ -108,6 +109,7 @@ public class JsonRpcHttpServiceTest {
protected static final Collection<RpcApi> JSON_RPC_APIS =
Arrays.asList(RpcApis.ETH, RpcApis.NET, RpcApis.WEB3, RpcApis.ADMIN);
protected final JsonRpcTestHelper testHelper = new JsonRpcTestHelper();
protected static final NatService natService = new NatService(Optional.empty());
@BeforeClass
public static void initServerAndClient() throws Exception {
@ -143,6 +145,7 @@ public class JsonRpcHttpServiceTest {
mock(JsonRpcConfiguration.class),
mock(WebSocketConfiguration.class),
mock(MetricsConfiguration.class),
natService,
new HashMap<>()));
service = createJsonRpcHttpService();
service.start().join();
@ -159,7 +162,7 @@ public class JsonRpcHttpServiceTest {
folder.newFolder().toPath(),
config,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);
@ -171,7 +174,7 @@ public class JsonRpcHttpServiceTest {
folder.newFolder().toPath(),
createJsonRpcConfig(),
new NoOpMetricsSystem(),
Optional.empty(),
natService,
rpcMethods,
HealthService.ALWAYS_HEALTHY,
HealthService.ALWAYS_HEALTHY);

@ -34,6 +34,10 @@ import org.hyperledger.besu.ethereum.core.Hash;
import org.hyperledger.besu.ethereum.p2p.network.P2PNetwork;
import org.hyperledger.besu.ethereum.p2p.peers.DefaultPeer;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.math.BigInteger;
import java.util.Collections;
@ -55,6 +59,7 @@ public class AdminNodeInfoTest {
@Mock private P2PNetwork p2pNetwork;
@Mock private Blockchain blockchain;
@Mock private BlockchainQueries blockchainQueries;
@Mock private NatService natService;
private AdminNodeInfo method;
@ -85,7 +90,8 @@ public class AdminNodeInfoTest {
BigInteger.valueOf(2018),
genesisConfigOptions,
p2pNetwork,
blockchainQueries);
blockchainQueries,
natService);
}
@Test
@ -127,6 +133,57 @@ public class AdminNodeInfoTest {
assertThat(actual.getResult()).isEqualTo(expected);
}
@Test
public void shouldReturnCorrectResultWhenIsNatEnvironment() {
when(p2pNetwork.isP2pEnabled()).thenReturn(true);
when(p2pNetwork.getLocalEnode()).thenReturn(Optional.of(defaultPeer.getEnodeURL()));
when(natService.queryExternalIPAddress()).thenReturn(Optional.of("3.4.5.6"));
when(natService.getPortMapping(NatServiceType.DISCOVERY, NetworkProtocol.UDP))
.thenReturn(
Optional.of(
new NatPortMapping(
NatServiceType.DISCOVERY, NetworkProtocol.UDP, "", "", 8080, 8080)));
when(natService.getPortMapping(NatServiceType.RLPX, NetworkProtocol.TCP))
.thenReturn(
Optional.of(
new NatPortMapping(NatServiceType.RLPX, NetworkProtocol.TCP, "", "", 8081, 8081)));
final JsonRpcRequestContext request = adminNodeInfo();
final Map<String, Object> expected = new HashMap<>();
expected.put(
"enode",
"enode://0f1b319e32017c3fcb221841f0f978701b4e9513fe6a567a2db43d43381a9c7e3dfe7cae13cbc2f56943400bacaf9082576ab087cd51983b17d729ae796f6807@3.4.5.6:8081?discport=8080");
expected.put(
"id",
"0f1b319e32017c3fcb221841f0f978701b4e9513fe6a567a2db43d43381a9c7e3dfe7cae13cbc2f56943400bacaf9082576ab087cd51983b17d729ae796f6807");
expected.put("ip", "3.4.5.6");
expected.put("listenAddr", "3.4.5.6:8081");
expected.put("name", "testnet/1.0/this/that");
expected.put("ports", ImmutableMap.of("discovery", 8080, "listener", 8081));
expected.put(
"protocols",
ImmutableMap.of(
"eth",
ImmutableMap.of(
"config",
genesisConfigOptions.asMap(),
"difficulty",
1L,
"genesis",
Hash.EMPTY.toString(),
"head",
Hash.EMPTY.toString(),
"network",
BigInteger.valueOf(2018))));
final JsonRpcResponse response = method.response(request);
assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class);
final JsonRpcSuccessResponse actual = (JsonRpcSuccessResponse) response;
assertThat(actual.getResult()).isEqualTo(expected);
}
@Test
public void handlesLocalEnodeWithListeningAndDiscoveryDisabled() {
final EnodeURL localEnode =

@ -29,7 +29,7 @@ import org.hyperledger.besu.ethereum.p2p.discovery.internal.TimerUtil;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.p2p.peers.PeerId;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.util.NetworkUtility;
import org.hyperledger.besu.util.Subscribers;
@ -41,7 +41,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -65,7 +64,7 @@ public abstract class PeerDiscoveryAgent {
protected final List<DiscoveryPeer> bootstrapPeers;
private final List<PeerRequirement> peerRequirements = new CopyOnWriteArrayList<>();
private final PeerPermissions peerPermissions;
private final Optional<UpnpNatManager> natManager;
private final NatService natService;
private final MetricsSystem metricsSystem;
/* The peer controller, which takes care of the state machine of peers. */
protected Optional<PeerDiscoveryController> controller = Optional.empty();
@ -88,7 +87,7 @@ public abstract class PeerDiscoveryAgent {
final SECP256K1.KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final MetricsSystem metricsSystem) {
this.metricsSystem = metricsSystem;
checkArgument(keyPair != null, "keypair cannot be null");
@ -97,7 +96,7 @@ public abstract class PeerDiscoveryAgent {
validateConfiguration(config);
this.peerPermissions = peerPermissions;
this.natManager = natManager;
this.natService = natService;
this.bootstrapPeers =
config.getBootnodes().stream().map(DiscoveryPeer::fromEnode).collect(Collectors.toList());
@ -125,24 +124,8 @@ public abstract class PeerDiscoveryAgent {
LOG.info("Starting peer discovery agent on host={}, port={}", host, port);
// override advertised host if we detect an external IP address via NAT manager
String externalIpAddress = null;
if (natManager.isPresent()) {
try {
final int timeoutSeconds = 60;
LOG.info("Waiting for up to {} seconds to detect external IP address...", timeoutSeconds);
externalIpAddress =
natManager.get().queryExternalIPAddress().get(timeoutSeconds, TimeUnit.SECONDS);
} catch (Exception e) {
LOG.warn(
"Caught exception while trying to query NAT external IP address (ignoring): {}", e);
}
}
final String advertisedAddress =
(null != externalIpAddress && !externalIpAddress.equals(""))
? externalIpAddress
: config.getAdvertisedHost();
natService.queryExternalIPAddress().orElse(config.getAdvertisedHost());
return listenForConnections()
.thenApply(

@ -25,7 +25,7 @@ import org.hyperledger.besu.ethereum.p2p.discovery.internal.TimerUtil;
import org.hyperledger.besu.ethereum.p2p.discovery.internal.VertxTimerUtil;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.metrics.BesuMetricCategory;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.plugin.services.MetricsSystem;
import org.hyperledger.besu.util.NetworkUtility;
@ -33,7 +33,6 @@ import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.CompletableFuture;
import java.util.function.IntSupplier;
@ -62,9 +61,9 @@ public class VertxPeerDiscoveryAgent extends PeerDiscoveryAgent {
final KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final MetricsSystem metricsSystem) {
super(keyPair, config, peerPermissions, natManager, metricsSystem);
super(keyPair, config, peerPermissions, natService, metricsSystem);
checkArgument(vertx != null, "vertx instance cannot be null");
this.vertx = vertx;

@ -41,6 +41,10 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.RlpxAgent;
import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.plugin.services.MetricsSystem;
@ -123,8 +127,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
private final PeerPermissions peerPermissions;
private final MaintainedPeers maintainedPeers;
private final Optional<UpnpNatManager> natManager;
private Optional<String> natExternalAddress;
private final NatService natService;
private OptionalLong peerBondedObserverId = OptionalLong.empty();
@ -145,7 +148,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
* @param keyPair This node's keypair.
* @param config The network configuration to use.
* @param peerPermissions An object that determines whether peers are allowed to connect
* @param natManager The NAT environment manager.
* @param natService The NAT environment manager.
* @param maintainedPeers A collection of peers for which we are expected to maintain connections
* @param reputationManager An object that inspect disconnections for misbehaving peers that can
* then be blacklisted.
@ -157,7 +160,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
final SECP256K1.KeyPair keyPair,
final NetworkingConfiguration config,
final PeerPermissions peerPermissions,
final Optional<UpnpNatManager> natManager,
final NatService natService,
final MaintainedPeers maintainedPeers,
final PeerReputationManager reputationManager) {
@ -165,7 +168,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
this.peerDiscoveryAgent = peerDiscoveryAgent;
this.rlpxAgent = rlpxAgent;
this.config = config;
this.natManager = natManager;
this.natService = natService;
this.maintainedPeers = maintainedPeers;
this.nodeId = keyPair.getPublicKey().getEncodedBytes();
@ -174,8 +177,6 @@ public class DefaultP2PNetwork implements P2PNetwork {
final int maxPeers = config.getRlpx().getMaxPeers();
peerDiscoveryAgent.addPeerRequirement(() -> rlpxAgent.getConnectionCount() >= maxPeers);
subscribeDisconnect(reputationManager);
natExternalAddress = Optional.empty();
}
public static Builder builder() {
@ -189,6 +190,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
return;
}
final String address = config.getDiscovery().getAdvertisedHost();
final int configuredDiscoveryPort = config.getDiscovery().getBindPort();
final int configuredRlpxPort = config.getRlpx().getBindPort();
@ -201,11 +203,17 @@ public class DefaultP2PNetwork implements P2PNetwork {
: configuredDiscoveryPort)
.join();
if (natManager.isPresent()) {
this.configureNatEnvironment(listeningPort, discoveryPort);
}
natService.ifNatEnvironment(
NatMethod.UPNP,
natManager -> {
UpnpNatManager upnpNatManager = (UpnpNatManager) natManager;
upnpNatManager.requestPortForward(
discoveryPort, NetworkProtocol.UDP, NatServiceType.DISCOVERY);
upnpNatManager.requestPortForward(
listeningPort, NetworkProtocol.TCP, NatServiceType.RLPX);
});
setLocalNode(listeningPort, discoveryPort);
setLocalNode(address, listeningPort, discoveryPort);
peerBondedObserverId =
OptionalLong.of(peerDiscoveryAgent.observePeerBondedEvents(this::handlePeerBondedEvent));
@ -352,13 +360,15 @@ public class DefaultP2PNetwork implements P2PNetwork {
return Optional.of(localNode.getPeer().getEnodeURL());
}
private void setLocalNode(final int listeningPort, final int discoveryPort) {
private void setLocalNode(
final String address, final int listeningPort, final int discoveryPort) {
if (localNode.isReady()) {
// Already set
return;
}
String advertisedAddress = natExternalAddress.orElse(config.getDiscovery().getAdvertisedHost());
// override advertised host if we detect an external IP address via NAT manager
final String advertisedAddress = natService.queryExternalIPAddress().orElse(address);
final EnodeURL localEnode =
EnodeURL.builder()
@ -372,38 +382,6 @@ public class DefaultP2PNetwork implements P2PNetwork {
localNode.setEnode(localEnode);
}
private void configureNatEnvironment(final int listeningPort, final int discoveryPort) {
final CompletableFuture<String> natQueryFuture =
this.natManager.orElseThrow().queryExternalIPAddress();
String externalAddress = null;
try {
final int timeoutSeconds = 60;
LOG.info(
"Querying NAT environment for external IP address, timeout "
+ timeoutSeconds
+ " seconds...");
externalAddress = natQueryFuture.get(timeoutSeconds, TimeUnit.SECONDS);
// if we're in a NAT environment, request port forwards for every port we
// intend to bind to
if (externalAddress != null) {
LOG.info("External IP detected: " + externalAddress);
this.natManager
.get()
.requestPortForward(discoveryPort, UpnpNatManager.Protocol.UDP, "besu-discovery");
this.natManager
.get()
.requestPortForward(listeningPort, UpnpNatManager.Protocol.TCP, "besu-rlpx");
} else {
LOG.info("No external IP detected within timeout.");
}
} catch (final Exception e) {
LOG.error("Error configuring NAT environment", e);
}
natExternalAddress = Optional.ofNullable(externalAddress);
}
public static class Builder {
private Vertx vertx;
@ -417,7 +395,8 @@ public class DefaultP2PNetwork implements P2PNetwork {
private MaintainedPeers maintainedPeers = new MaintainedPeers();
private PeerPermissions peerPermissions = PeerPermissions.noop();
private Optional<UpnpNatManager> natManager = Optional.empty();
private NatService natService = new NatService(Optional.empty());
private MetricsSystem metricsSystem;
public P2PNetwork build() {
@ -445,7 +424,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
keyPair,
config,
peerPermissions,
natManager,
natService,
maintainedPeers,
reputationManager);
}
@ -463,7 +442,7 @@ public class DefaultP2PNetwork implements P2PNetwork {
private PeerDiscoveryAgent createDiscoveryAgent() {
return new VertxPeerDiscoveryAgent(
vertx, keyPair, config.getDiscovery(), peerPermissions, natManager, metricsSystem);
vertx, keyPair, config.getDiscovery(), peerPermissions, natService, metricsSystem);
}
private RlpxAgent createRlpxAgent(
@ -531,13 +510,9 @@ public class DefaultP2PNetwork implements P2PNetwork {
return this;
}
public Builder natManager(final UpnpNatManager natManager) {
this.natManager = Optional.ofNullable(natManager);
return this;
}
public Builder natManager(final Optional<UpnpNatManager> natManager) {
this.natManager = natManager;
public Builder natService(final NatService natService) {
checkNotNull(natService);
this.natService = natService;
return this;
}

@ -27,12 +27,14 @@ import org.hyperledger.besu.ethereum.p2p.discovery.internal.PingPacketData;
import org.hyperledger.besu.ethereum.p2p.peers.EnodeURL;
import org.hyperledger.besu.ethereum.p2p.peers.Peer;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.nat.NatService;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -247,12 +249,13 @@ public class PeerDiscoveryTestHelper {
public MockPeerDiscoveryAgent build() {
final int port = bindPort.orElseGet(nextAvailablePort::incrementAndGet);
final DiscoveryConfiguration config = new DiscoveryConfiguration();
final NatService natService = new NatService(Optional.empty());
config.setBootnodes(bootnodes);
config.setAdvertisedHost(advertisedHost);
config.setBindPort(port);
config.setActive(active);
return new MockPeerDiscoveryAgent(keyPair, config, peerPermissions, agents);
return new MockPeerDiscoveryAgent(keyPair, config, peerPermissions, agents, natService);
}
}
}

@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.p2p.discovery.PeerDiscoveryAgent;
import org.hyperledger.besu.ethereum.p2p.discovery.internal.PeerDiscoveryController.AsyncExecutor;
import org.hyperledger.besu.ethereum.p2p.permissions.PeerPermissions;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.nat.NatService;
import java.net.InetSocketAddress;
import java.util.ArrayDeque;
@ -28,7 +29,6 @@ import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.apache.tuweni.bytes.Bytes;
@ -43,8 +43,9 @@ public class MockPeerDiscoveryAgent extends PeerDiscoveryAgent {
final KeyPair keyPair,
final DiscoveryConfiguration config,
final PeerPermissions peerPermissions,
final Map<Bytes, MockPeerDiscoveryAgent> agentNetwork) {
super(keyPair, config, peerPermissions, Optional.empty(), new NoOpMetricsSystem());
final Map<Bytes, MockPeerDiscoveryAgent> agentNetwork,
final NatService natService) {
super(keyPair, config, peerPermissions, natService, new NoOpMetricsSystem());
this.agentNetwork = agentNetwork;
}

@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -43,8 +44,10 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MockSubProtocol;
import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.NatService;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import org.hyperledger.besu.nat.upnp.UpnpNatManager.Protocol;
import java.util.ArrayList;
import java.util.List;
@ -61,6 +64,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.StrictStubs.class)
@ -69,7 +73,6 @@ public final class DefaultP2PNetworkTest {
final MaintainedPeers maintainedPeers = new MaintainedPeers();
@Mock PeerDiscoveryAgent discoveryAgent;
@Mock RlpxAgent rlpxAgent;
@Mock UpnpNatManager natManager;
private final ArgumentCaptor<PeerBondedObserver> discoverySubscriberCaptor =
ArgumentCaptor.forClass(PeerBondedObserver.class);
@ -217,15 +220,20 @@ public final class DefaultP2PNetworkTest {
config.getRlpx().setBindPort(30303);
config.getDiscovery().setBindPort(30301);
when(natManager.queryExternalIPAddress())
final UpnpNatManager upnpNatManager = mock(UpnpNatManager.class);
when(upnpNatManager.getNatMethod()).thenReturn(NatMethod.UPNP);
when(upnpNatManager.queryExternalIPAddress())
.thenReturn(CompletableFuture.completedFuture(externalIp));
final P2PNetwork network = builder().natManager(natManager).build();
final NatService natService = Mockito.spy(new NatService(Optional.of(upnpNatManager)));
final P2PNetwork network = builder().natService(natService).build();
network.start();
verify(natManager)
.requestPortForward(eq(config.getRlpx().getBindPort()), eq(Protocol.TCP), any());
verify(natManager)
.requestPortForward(eq(config.getDiscovery().getBindPort()), eq(Protocol.UDP), any());
verify(upnpNatManager)
.requestPortForward(eq(config.getRlpx().getBindPort()), eq(NetworkProtocol.TCP), any());
verify(upnpNatManager)
.requestPortForward(
eq(config.getDiscovery().getBindPort()), eq(NetworkProtocol.UDP), any());
Assertions.assertThat(network.getLocalEnode().get().getIpAsString()).isEqualTo(externalIp);
}

@ -36,6 +36,7 @@ import org.hyperledger.besu.ethereum.retesteth.methods.TestModifyTimestamp;
import org.hyperledger.besu.ethereum.retesteth.methods.TestRewindToBlock;
import org.hyperledger.besu.ethereum.retesteth.methods.TestSetChainParams;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.hyperledger.besu.nat.NatService;
import java.util.Arrays;
import java.util.Map;
@ -59,6 +60,8 @@ public class RetestethService {
retestethContext = new RetestethContext();
final BlockResultFactory blockResult = new BlockResultFactory();
final NatService natService = new NatService(Optional.empty());
final Map<String, JsonRpcMethod> jsonRpcMethods =
mapOf(
new Web3ClientVersion(clientVersion),
@ -87,7 +90,7 @@ public class RetestethService {
retestethConfiguration.getDataPath(),
jsonRpcConfiguration,
new NoOpMetricsSystem(),
Optional.empty(),
natService,
jsonRpcMethods,
new HealthService(new LivenessCheck()),
HealthService.ALWAYS_HEALTHY);

@ -16,6 +16,8 @@ package org.hyperledger.besu.nat;
public enum NatMethod {
UPNP,
MANUAL,
AUTO,
NONE;
public static NatMethod fromString(final String str) {

@ -0,0 +1,204 @@
/*
* 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.nat;
import static com.google.common.base.Preconditions.checkNotNull;
import org.hyperledger.besu.nat.core.AutoDetectionResult;
import org.hyperledger.besu.nat.core.NatManager;
import org.hyperledger.besu.nat.core.NatMethodAutoDetection;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** Utility class to help interacting with various {@link NatManager}. */
public class NatService {
protected static final Logger LOG = LogManager.getLogger();
private final NatMethod currentNatMethod;
private final Optional<NatManager> currentNatManager;
public NatService(final Optional<NatManager> natManager) {
this.currentNatMethod = retrieveNatMethod(natManager);
this.currentNatManager = natManager;
}
/**
* Returns whether or not the Besu node is running under a NAT environment.
*
* @return true if Besu node is running under NAT environment, false otherwise.
*/
public boolean isNatEnvironment() {
return currentNatMethod != NatMethod.NONE;
}
/**
* If nat environment is present, performs the given action, otherwise does nothing.
*
* @param natMethod specific on which only this action must be performed
* @param action the action to be performed, if a nat environment is present
*/
public void ifNatEnvironment(
final NatMethod natMethod, final Consumer<? super NatManager> action) {
if (isNatEnvironment()) {
currentNatManager.filter(s -> natMethod.equals(s.getNatMethod())).ifPresent(action);
}
}
/**
* Returns the NAT method.
*
* @return the current NatMethod.
*/
public NatMethod getNatMethod() {
return currentNatMethod;
}
/**
* Returns the NAT manager associated to the current NAT method.
*
* @return an {@link Optional} wrapping the {@link NatManager} or empty if not found.
*/
public Optional<NatManager> getNatManager() {
return currentNatManager;
}
/** Starts the manager or service. */
public void start() {
if (isNatEnvironment()) {
try {
getNatManager().orElseThrow().start();
} catch (Exception e) {
LOG.warn("Caught exception while trying to start the manager or service.", e);
}
} else {
LOG.info("No NAT environment detected so no service could be started");
}
}
/** Stops the manager or service. */
public void stop() {
if (isNatEnvironment()) {
try {
getNatManager().orElseThrow().stop();
} catch (Exception e) {
LOG.warn("Caught exception while trying to stop the manager or service", e);
}
} else {
LOG.info("No NAT environment detected so no service could be stopped");
}
}
/**
* Returns a {@link Optional} wrapping the advertised IP address.
*
* @return The advertised IP address wrapped in a {@link Optional}. Empty if
* `isNatExternalIpUsageEnabled` is false
*/
public Optional<String> queryExternalIPAddress() {
if (isNatEnvironment()) {
try {
final NatManager natManager = getNatManager().orElseThrow();
LOG.info(
"Waiting for up to {} seconds to detect external IP address...",
NatManager.TIMEOUT_SECONDS);
return Optional.of(
natManager.queryExternalIPAddress().get(NatManager.TIMEOUT_SECONDS, TimeUnit.SECONDS));
} catch (Exception e) {
LOG.warn(
"Caught exception while trying to query NAT external IP address (ignoring): {}", e);
}
}
return Optional.empty();
}
/**
* Returns a {@link Optional} wrapping the local IP address.
*
* @return The local IP address wrapped in a {@link Optional}.
*/
public Optional<String> queryLocalIPAddress() throws RuntimeException {
if (isNatEnvironment()) {
try {
final NatManager natManager = getNatManager().orElseThrow();
LOG.info(
"Waiting for up to {} seconds to detect external IP address...",
NatManager.TIMEOUT_SECONDS);
return Optional.of(
natManager.queryLocalIPAddress().get(NatManager.TIMEOUT_SECONDS, TimeUnit.SECONDS));
} catch (Exception e) {
LOG.warn("Caught exception while trying to query local IP address (ignoring): {}", e);
}
}
return Optional.empty();
}
/**
* Returns the port mapping associated to the passed service type.
*
* @param serviceType The service type {@link NatServiceType}.
* @param networkProtocol The network protocol {@link NetworkProtocol}.
* @return The port mapping {@link NatPortMapping}
*/
public Optional<NatPortMapping> getPortMapping(
final NatServiceType serviceType, final NetworkProtocol networkProtocol) {
if (isNatEnvironment()) {
try {
final NatManager natManager = getNatManager().orElseThrow();
return Optional.of(natManager.getPortMapping(serviceType, networkProtocol));
} catch (Exception e) {
LOG.warn("Caught exception while trying to query port mapping (ignoring): {}", e);
}
}
return Optional.empty();
}
/**
* Retrieve the current NatMethod.
*
* @param natManager The natManager wrapped in a {@link Optional}.
* @return the current NatMethod.
*/
private NatMethod retrieveNatMethod(final Optional<NatManager> natManager) {
return natManager.map(NatManager::getNatMethod).orElse(NatMethod.NONE);
}
/**
* Attempts to automatically detect the Nat method being used by the node.
*
* @param natMethodAutoDetections list of nat method auto detections
* @return a {@link NatMethod} equal to NONE if no Nat method has been detected automatically.
*/
public static NatMethod autoDetectNatMethod(
final NatMethodAutoDetection... natMethodAutoDetections) {
checkNotNull(natMethodAutoDetections);
for (NatMethodAutoDetection autoDetection : natMethodAutoDetections) {
final AutoDetectionResult result = autoDetection.shouldBeThisNatMethod();
if (result.isDetectedNatMethod()) {
return result.getNatMethod();
}
}
return NatMethod.NONE;
}
}

@ -0,0 +1,127 @@
/*
* 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.nat.core;
import static com.google.common.base.Preconditions.checkState;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public abstract class AbstractNatManager implements NatManager {
protected static final Logger LOG = LogManager.getLogger();
protected final NatMethod natMethod;
protected final AtomicBoolean started = new AtomicBoolean();
protected AbstractNatManager(final NatMethod natMethod) {
this.natMethod = natMethod;
}
protected abstract void doStart();
protected abstract void doStop();
protected abstract CompletableFuture<String> retrieveExternalIPAddress();
@Override
public NatMethod getNatMethod() {
return natMethod;
}
@Override
public boolean isStarted() {
return started.get();
}
@Override
public CompletableFuture<String> queryExternalIPAddress() {
checkState(isStarted(), "Cannot call queryExternalIPAddress() when in stopped state");
return retrieveExternalIPAddress();
}
@Override
public CompletableFuture<String> queryLocalIPAddress() {
final CompletableFuture<String> future = new CompletableFuture<>();
Executors.newCachedThreadPool()
.submit(
() -> {
try {
future.complete(InetAddress.getLocalHost().getHostAddress());
} catch (UnknownHostException e) {
future.completeExceptionally(e);
}
});
return future;
}
@Override
public void start() {
if (started.compareAndSet(false, true)) {
doStart();
} else {
LOG.warn("Attempt to start an already-started {}", getClass().getSimpleName());
}
}
@Override
public void stop() {
if (started.compareAndSet(true, false)) {
doStop();
} else {
LOG.warn("Attempt to stop an already-stopped {}", getClass().getSimpleName());
}
}
@Override
public NatPortMapping getPortMapping(
final NatServiceType serviceType, final NetworkProtocol networkProtocol) {
try {
final List<NatPortMapping> natPortMappings =
getPortMappings().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
final Optional<NatPortMapping> foundPortMapping =
natPortMappings.stream()
.filter(
c ->
c.getNatServiceType().equals(serviceType)
&& c.getProtocol().equals(networkProtocol))
.findFirst();
return foundPortMapping.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Required service type not found : %s %s", serviceType, networkProtocol)));
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(
String.format("Unable to retrieve the service type : %s", serviceType.toString()));
}
}
}

@ -0,0 +1,37 @@
/*
* 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.nat.core;
import org.hyperledger.besu.nat.NatMethod;
public class AutoDetectionResult {
private final NatMethod natMethod;
private final boolean isDetectedNatMethod;
public AutoDetectionResult(final NatMethod natMethod, final boolean isDetectedNatMethod) {
this.natMethod = natMethod;
this.isDetectedNatMethod = isDetectedNatMethod;
}
public NatMethod getNatMethod() {
return natMethod;
}
public boolean isDetectedNatMethod() {
return isDetectedNatMethod;
}
}

@ -0,0 +1,83 @@
/*
* 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.nat.core;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* This class describes the behaviour of any supported NAT manager. Internal API to support Network
* Address Translation (NAT) technologies in Besu.
*/
public interface NatManager {
int TIMEOUT_SECONDS = 60;
/**
* Returns the NAT method associated to this manager.
*
* @return the {@link NatMethod}
*/
NatMethod getNatMethod();
/** Starts the manager or service. */
void start();
/** Stops the manager or service. */
void stop();
/**
* Returns whether or not the manager is started.
*
* @return true if started, false otherwise.
*/
boolean isStarted();
/**
* Returns a {@link java.util.concurrent.Future} wrapping the local IP address.
*
* @return The local IP address wrapped in a {@link java.util.concurrent.Future}.
*/
CompletableFuture<String> queryLocalIPAddress();
/**
* Returns a {@link java.util.concurrent.Future} wrapping the external IP address.
*
* @return The external IP address wrapped in a {@link java.util.concurrent.Future}.
*/
CompletableFuture<String> queryExternalIPAddress();
/**
* Returns all known port mappings.
*
* @return The known port mappings wrapped in a {@link java.util.concurrent.Future}.
*/
CompletableFuture<List<NatPortMapping>> getPortMappings();
/**
* Returns the port mapping associated to the passed service type.
*
* @param serviceType The service type {@link NatServiceType}.
* @param networkProtocol The network protocol {@link NetworkProtocol}.
* @return The port mapping {@link NatPortMapping}
*/
NatPortMapping getPortMapping(
final NatServiceType serviceType, final NetworkProtocol networkProtocol);
}

@ -0,0 +1,22 @@
/*
* 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.nat.core;
@FunctionalInterface
public interface NatMethodAutoDetection {
AutoDetectionResult shouldBeThisNatMethod();
}

@ -0,0 +1,73 @@
/*
* 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.nat.core.domain;
/** This class describes a NAT configuration. */
public class NatPortMapping {
private final NatServiceType natServiceType;
private final NetworkProtocol protocol;
private final String internalHost;
private final String remoteHost;
private final int externalPort;
private final int internalPort;
public NatPortMapping(
final NatServiceType natServiceType,
final NetworkProtocol protocol,
final String internalHost,
final String remoteHost,
final int externalPort,
final int internalPort) {
this.natServiceType = natServiceType;
this.protocol = protocol;
this.internalHost = internalHost;
this.remoteHost = remoteHost;
this.externalPort = externalPort;
this.internalPort = internalPort;
}
public NatServiceType getNatServiceType() {
return natServiceType;
}
public NetworkProtocol getProtocol() {
return protocol;
}
public String getInternalHost() {
return internalHost;
}
public String getRemoteHost() {
return remoteHost;
}
public int getExternalPort() {
return externalPort;
}
public int getInternalPort() {
return internalPort;
}
@Override
public String toString() {
return String.format(
"[%s - %s] %s:%d ==> %s:%d",
natServiceType, protocol, internalHost, internalPort, remoteHost, externalPort);
}
}

@ -0,0 +1,59 @@
/*
* 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.nat.core.domain;
/**
* This enum describes the types of services that could be impacted by the {@link
* org.hyperledger.besu.nat.NatMethod} used by the Besu node.
*
* <ul>
* <li><b>JSON_RPC:</b> Ethereum JSON-RPC HTTP service.
* <li><b>RLPX:</b> Peer to Peer network layer.
* <li><b>DISCOVERY:</b> Peer to Peer discovery layer.
* </ul>
*/
public enum NatServiceType {
JSON_RPC("json-rpc"),
RLPX("rlpx"),
DISCOVERY("discovery");
private final String value;
NatServiceType(final String value) {
this.value = value;
}
/**
* Parses and returns corresponding enum value to the passed method name. This method throws an
* {@link IllegalStateException} if the method name is invalid.
*
* @param natServiceTypeName The name of the NAT service type.
* @return The corresponding {@link NatServiceType}
*/
public static NatServiceType fromString(final String natServiceTypeName) {
for (final NatServiceType mode : NatServiceType.values()) {
if (mode.getValue().equalsIgnoreCase(natServiceTypeName)) {
return mode;
}
}
throw new IllegalStateException(
String.format("Invalid NAT service type provided: %s", natServiceTypeName));
}
public String getValue() {
return value;
}
}

@ -0,0 +1,29 @@
/*
* 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.nat.core.domain;
/**
* This enum describes all supported NAT methods in Besu.
*
* <ul>
* <li><b>TCP:</b> Transmission Control Protocol.
* <li><b>UDP:</b> User Datagram Protocol.
* </ul>
*/
public enum NetworkProtocol {
TCP,
UDP
}

@ -0,0 +1,99 @@
/*
* 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.nat.manual;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.AbstractNatManager;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* This class describes the behaviour of the Manual NAT manager. Manual Nat manager add the ability
* to explicitly configure the external IP and Ports to broadcast without regards to NAT or other
* considerations.
*/
public class ManualNatManager extends AbstractNatManager {
private final String advertisedHost;
private final int p2pPort;
private final int rpcHttpPort;
private final List<NatPortMapping> forwardedPorts;
public ManualNatManager(final String advertisedHost, final int p2pPort, final int rpcHttpPort) {
super(NatMethod.MANUAL);
this.advertisedHost = advertisedHost;
this.p2pPort = p2pPort;
this.rpcHttpPort = rpcHttpPort;
this.forwardedPorts = buildForwardedPorts();
}
private List<NatPortMapping> buildForwardedPorts() {
try {
final String internalHost = queryLocalIPAddress().get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
return Arrays.asList(
new NatPortMapping(
NatServiceType.DISCOVERY,
NetworkProtocol.UDP,
internalHost,
advertisedHost,
p2pPort,
p2pPort),
new NatPortMapping(
NatServiceType.RLPX,
NetworkProtocol.TCP,
internalHost,
advertisedHost,
p2pPort,
p2pPort),
new NatPortMapping(
NatServiceType.JSON_RPC,
NetworkProtocol.TCP,
internalHost,
advertisedHost,
rpcHttpPort,
rpcHttpPort));
} catch (Exception e) {
LOG.warn("Failed to create forwarded port list", e);
}
return Collections.emptyList();
}
@Override
protected void doStart() {
LOG.info("Starting Manual NatManager");
}
@Override
protected void doStop() {
LOG.info("Stopping Manual NatManager");
}
@Override
protected CompletableFuture<String> retrieveExternalIPAddress() {
return CompletableFuture.completedFuture(advertisedHost);
}
@Override
public CompletableFuture<List<NatPortMapping>> getPortMappings() {
return CompletableFuture.completedFuture(forwardedPorts);
}
}

@ -17,6 +17,12 @@ package org.hyperledger.besu.nat.upnp;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.AbstractNatManager;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -49,12 +55,11 @@ import org.jupnp.support.model.PortMapping;
* Manages underlying UPnP library "jupnp" and provides abstractions for asynchronously interacting
* with the NAT environment through UPnP.
*/
public class UpnpNatManager {
public class UpnpNatManager extends AbstractNatManager {
protected static final Logger LOG = LogManager.getLogger();
static final String SERVICE_TYPE_WAN_IP_CONNECTION = "WANIPConnection";
private boolean started = false;
private final UpnpService upnpService;
private final RegistryListener registryListener;
@ -62,15 +67,9 @@ public class UpnpNatManager {
private final CompletableFuture<String> externalIpQueryFuture = new CompletableFuture<>();
private final Map<String, CompletableFuture<RemoteService>> recognizedServices = new HashMap<>();
private final List<PortMapping> forwardedPorts = new ArrayList<>();
private final List<NatPortMapping> forwardedPorts = new ArrayList<>();
private Optional<String> discoveredOnLocalAddress = Optional.empty();
/** Mirrored enum from PortMapping.Protocol to avoid bleeding jupnp types into calling code */
public enum Protocol {
UDP,
TCP
}
/** Empty constructor. Creates in instance of UpnpServiceImpl. */
public UpnpNatManager() {
// this(new UpnpServiceImpl(new DefaultUpnpServiceConfiguration()));
@ -90,6 +89,7 @@ public class UpnpNatManager {
* @param service is the desired instance of UpnpService.
*/
UpnpNatManager(final UpnpService service) {
super(NatMethod.UPNP);
upnpService = service;
// prime our recognizedServices map so we can use its key-set later
@ -111,19 +111,12 @@ public class UpnpNatManager {
*
* @throws IllegalStateException if already started.
*/
public synchronized void start() {
if (started) {
LOG.warn("Attempt to start an already-started {}", getClass().getSimpleName());
return;
}
@Override
public synchronized void doStart() {
LOG.info("Starting UPnP Service");
upnpService.startup();
upnpService.getRegistry().addListener(registryListener);
initiateExternalIpQuery();
started = true;
}
/**
@ -131,12 +124,8 @@ public class UpnpNatManager {
*
* @throws IllegalStateException if stopped.
*/
public synchronized void stop() {
if (!started) {
LOG.warn("Attempt to stop an already-stopped {}", getClass().getSimpleName());
return;
}
@Override
public synchronized void doStop() {
CompletableFuture<Void> portForwardReleaseFuture = releaseAllPortForwards();
try {
LOG.info("Allowing 3 seconds to release all port forwards...");
@ -151,8 +140,6 @@ public class UpnpNatManager {
upnpService.getRegistry().removeListener(registryListener);
upnpService.shutdown();
started = false;
}
/**
@ -172,10 +159,7 @@ public class UpnpNatManager {
*/
@VisibleForTesting
synchronized CompletableFuture<RemoteService> getWANIPConnectionService() {
if (!started) {
throw new IllegalStateException(
"Cannot call getWANIPConnectionService() when in stopped state");
}
checkState(isStarted(), "Cannot call getWANIPConnectionService() when in stopped state");
return getService(SERVICE_TYPE_WAN_IP_CONNECTION);
}
@ -197,13 +181,22 @@ public class UpnpNatManager {
*
* @return A CompletableFuture that can be used to query the result (or error).
*/
public synchronized CompletableFuture<String> queryExternalIPAddress() {
if (!started) {
throw new IllegalStateException("Cannot call queryExternalIPAddress() when in stopped state");
}
@Override
public CompletableFuture<String> retrieveExternalIPAddress() {
return externalIpQueryFuture.thenApply(x -> x);
}
/**
* Returns all known port mappings.
*
* @return The known port mappings wrapped in a {@link java.util.concurrent.Future}.
*/
@Override
public CompletableFuture<List<NatPortMapping>> getPortMappings() {
checkState(isStarted(), "Cannot call getPortMappings() when in stopped state");
return CompletableFuture.completedFuture(forwardedPorts);
}
/**
* Sends a UPnP request to the discovered IGD for the external ip address.
*
@ -277,12 +270,13 @@ public class UpnpNatManager {
* <p>In addition, port is used for both internal and external port values.
*
* @param port is the port to be used for both internal and external port values
* @param protocol is either UDP or TCP
* @param description is a free-form description, often displayed in router UIs
* @param protocol is either UDP or TCP {@link NetworkProtocol}
* @param serviceType {@link NatServiceType}, often displayed in router UIs
*/
public void requestPortForward(
final int port, final Protocol protocol, final String description) {
final int port, final NetworkProtocol protocol, final NatServiceType serviceType) {
checkState(isStarted(), "Cannot call requestPortForward() when in stopped state");
checkArgument(port != 0, "Cannot map to internal port zero.");
this.requestPortForward(
new PortMapping(
true,
@ -292,7 +286,7 @@ public class UpnpNatManager {
new UnsignedIntegerTwoBytes(port),
null,
toJupnpProtocol(protocol),
description));
serviceType.getValue()));
}
/**
@ -302,9 +296,6 @@ public class UpnpNatManager {
* @return A CompletableFuture that can be used to query the result (or error).
*/
private CompletableFuture<Void> requestPortForward(final PortMapping portMapping) {
checkArgument(
portMapping.getInternalPort().getValue() != 0, "Cannot map to internal port zero.");
checkState(started, "Cannot call requestPortForward() when in stopped state");
CompletableFuture<Void> upnpQueryFuture = new CompletableFuture<>();
@ -337,7 +328,17 @@ public class UpnpNatManager {
portMapping.getExternalPort());
synchronized (forwardedPorts) {
forwardedPorts.add(portMapping);
final NatServiceType natServiceType =
NatServiceType.fromString(portMapping.getDescription());
final NatPortMapping natPortMapping =
new NatPortMapping(
natServiceType,
NetworkProtocol.valueOf(portMapping.getProtocol().name()),
portMapping.getInternalClient(),
portMapping.getRemoteHost(),
portMapping.getExternalPort().getValue().intValue(),
portMapping.getInternalPort().getValue().intValue());
forwardedPorts.add(natPortMapping);
}
upnpQueryFuture.complete(null);
@ -384,10 +385,6 @@ public class UpnpNatManager {
* @return A CompletableFuture that will complete when all port forward requests have been made
*/
private CompletableFuture<Void> releaseAllPortForwards() {
if (!started) {
throw new IllegalStateException("Cannot call releaseAllPortForwards() when in stopped state");
}
// if we haven't observed the WANIPConnection service yet, we should have no port forwards to
// release
CompletableFuture<RemoteService> wanIPConnectionServiceFuture =
@ -402,9 +399,9 @@ public class UpnpNatManager {
boolean done = false;
while (!done) {
PortMapping portMapping = null;
NatPortMapping portMapping;
synchronized (forwardedPorts) {
if (forwardedPorts.size() == 0) {
if (forwardedPorts.isEmpty()) {
done = true;
continue;
}
@ -422,7 +419,7 @@ public class UpnpNatManager {
CompletableFuture<Void> future = new CompletableFuture<>();
PortMappingDelete callback =
new PortMappingDelete(service, portMapping) {
new PortMappingDelete(service, toJupnpPortMapping(portMapping)) {
/**
* Because the underlying jupnp library omits generics info in this method signature, we
* must too when we override it.
@ -496,7 +493,7 @@ public class UpnpNatManager {
}
}
private PortMapping.Protocol toJupnpProtocol(final Protocol protocol) {
private PortMapping.Protocol toJupnpProtocol(final NetworkProtocol protocol) {
switch (protocol) {
case UDP:
return PortMapping.Protocol.UDP;
@ -505,4 +502,16 @@ public class UpnpNatManager {
}
return null;
}
private PortMapping toJupnpPortMapping(final NatPortMapping natPortMapping) {
return new PortMapping(
true,
new UnsignedIntegerFourBytes(0),
null,
new UnsignedIntegerTwoBytes(natPortMapping.getExternalPort()),
new UnsignedIntegerTwoBytes(natPortMapping.getInternalPort()),
null,
toJupnpProtocol(natPortMapping.getProtocol()),
natPortMapping.getNatServiceType().getValue());
}
}

@ -0,0 +1,172 @@
/*
* 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.nat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.nat.core.AutoDetectionResult;
import org.hyperledger.besu.nat.core.NatManager;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import org.hyperledger.besu.nat.upnp.UpnpNatManager;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class NatServiceTest {
@Test
public void assertThatGetNatManagerReturnValidManager() {
final NatService natService = new NatService(Optional.of(new UpnpNatManager()));
assertThat(natService.getNatMethod()).isEqualTo(NatMethod.UPNP);
assertThat(natService.getNatManager()).containsInstanceOf(UpnpNatManager.class);
}
@Test
public void assertThatGetNatManagerNotReturnManagerWhenNatMethodIsNone() {
final NatService natService = new NatService(Optional.empty());
assertThat(natService.getNatMethod()).isEqualTo(NatMethod.NONE);
assertThat(natService.getNatManager()).isNotPresent();
}
@Test
public void assertThatIsNatEnvironmentReturnCorrectStatus() {
final NatService nonNatService = new NatService(Optional.empty());
assertThat(nonNatService.isNatEnvironment()).isFalse();
final NatService upnpNatService = new NatService(Optional.of(new UpnpNatManager()));
assertThat(upnpNatService.isNatEnvironment()).isTrue();
}
@Test
public void assertThatGetPortMappingWorksProperlyWithUpNp() {
final String externalIp = "127.0.0.3";
final NatPortMapping natPortMapping =
new NatPortMapping(
NatServiceType.DISCOVERY, NetworkProtocol.UDP, externalIp, externalIp, 1111, 1111);
final NatManager natManager = mock(NatManager.class);
when(natManager.getPortMapping(
natPortMapping.getNatServiceType(), natPortMapping.getProtocol()))
.thenReturn(natPortMapping);
when(natManager.getNatMethod()).thenReturn(NatMethod.UPNP);
final NatService natService = new NatService(Optional.of(natManager));
final Optional<NatPortMapping> portMapping =
natService.getPortMapping(natPortMapping.getNatServiceType(), natPortMapping.getProtocol());
verify(natManager)
.getPortMapping(natPortMapping.getNatServiceType(), natPortMapping.getProtocol());
assertThat(portMapping).contains(natPortMapping);
}
@Test
public void assertThatGetPortMappingWorksProperlyWithoutNat() {
final NatService natService = new NatService(Optional.empty());
final Optional<NatPortMapping> portMapping =
natService.getPortMapping(NatServiceType.DISCOVERY, NetworkProtocol.TCP);
assertThat(portMapping).isNotPresent();
}
@Test
public void assertQueryExternalIpWorksProperlyWithUpNp() {
final String externalIp = "127.0.0.3";
final NatManager natManager = mock(NatManager.class);
when(natManager.queryExternalIPAddress())
.thenReturn(CompletableFuture.completedFuture(externalIp));
when(natManager.getNatMethod()).thenReturn(NatMethod.UPNP);
final NatService natService = new NatService(Optional.of(natManager));
final Optional<String> resultIp = natService.queryExternalIPAddress();
verify(natManager).queryExternalIPAddress();
assertThat(resultIp).containsSame(externalIp);
}
@Test
public void assertThatQueryExternalIpWorksProperlyWithoutNat() {
final NatService natService = new NatService(Optional.empty());
final Optional<String> resultIp = natService.queryExternalIPAddress();
assertThat(resultIp).isNotPresent();
}
@Test
public void assertThatQueryLocalIPAddressWorksProperlyWithUpNp() {
final String externalIp = "127.0.0.3";
final NatManager natManager = mock(NatManager.class);
when(natManager.queryLocalIPAddress())
.thenReturn(CompletableFuture.completedFuture(externalIp));
when(natManager.getNatMethod()).thenReturn(NatMethod.UPNP);
final NatService natService = new NatService(Optional.of(natManager));
final Optional<String> resultIp = natService.queryLocalIPAddress();
verify(natManager).queryLocalIPAddress();
assertThat(resultIp).containsSame(externalIp);
}
@Test
public void assertThatQueryLocalIPAddressWorksProperlyWithoutNat() {
final NatService natService = new NatService(Optional.empty());
final Optional<String> resultIp = natService.queryLocalIPAddress();
assertThat(resultIp).isNotPresent();
}
@Test
public void givenOneAutoDetectionWorksWhenAutoDetectThenReturnCorrectNatMethod() {
final NatMethod natMethod =
NatService.autoDetectNatMethod(NatServiceTest::alwaysTrueShouldBeUpnpMethod);
assertThat(natMethod).isEqualTo(NatMethod.UPNP);
}
@Test
public void givenNoAutoDetectionWorksWhenAutoDetectThenReturnEmptyNatMethod() {
final NatMethod natMethod =
NatService.autoDetectNatMethod(NatServiceTest::alwaysFalseShouldBeUpnpMethod);
assertThat(natMethod).isEqualTo(NatMethod.NONE);
}
private static AutoDetectionResult alwaysTrueShouldBeUpnpMethod() {
return new AutoDetectionResult(NatMethod.UPNP, true);
}
private static AutoDetectionResult alwaysFalseShouldBeUpnpMethod() {
return new AutoDetectionResult(NatMethod.UPNP, false);
}
}

@ -0,0 +1,109 @@
/*
* 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.nat.core;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.hyperledger.besu.nat.NatMethod;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class AbstractNatManagerTest {
@Test
public void assertThatManagerIsStartedAfterStart() {
final AbstractNatManager natManager = buildNatManager(NatMethod.UPNP);
assertThat(natManager.isStarted()).isFalse();
natManager.start();
assertThat(natManager.isStarted()).isTrue();
}
@Test
public void assertThatManagerIsStoppedAfterStopped() {
final AbstractNatManager natManager = buildNatManager(NatMethod.UPNP);
assertThat(natManager.isStarted()).isFalse();
natManager.start();
assertThat(natManager.isStarted()).isTrue();
natManager.stop();
assertThat(natManager.isStarted()).isFalse();
}
@Test
public void assertThatDoStartIsCalledOnlyOnce() {
final AbstractNatManager natManager = Mockito.spy(buildNatManager(NatMethod.UPNP));
natManager.start();
natManager.start();
verify(natManager, times(2)).start();
verify(natManager).doStart();
}
@Test
public void assertThatDoStopIsCalledOnlyOnce() {
final AbstractNatManager natManager = Mockito.spy(buildNatManager(NatMethod.UPNP));
natManager.start();
natManager.stop();
natManager.stop();
verify(natManager).start();
verify(natManager).doStart();
verify(natManager, times(2)).stop();
verify(natManager).doStop();
}
@Test
public void assertThatManagerReturnValidNatMethod() {
assertThat(buildNatManager(NatMethod.UPNP).getNatMethod()).isEqualTo(NatMethod.UPNP);
}
@Test
public void assertThatManagerReturnValidLocalIpAddress()
throws UnknownHostException, ExecutionException, InterruptedException {
final String hostAddress = InetAddress.getLocalHost().getHostAddress();
assertThat(buildNatManager(NatMethod.UPNP).queryLocalIPAddress().get()).isEqualTo(hostAddress);
}
private static AbstractNatManager buildNatManager(final NatMethod natMethod) {
return new AbstractNatManager(natMethod) {
@Override
public void doStart() {}
@Override
public void doStop() {}
@Override
protected CompletableFuture<String> retrieveExternalIPAddress() {
return new CompletableFuture<>();
}
@Override
public CompletableFuture<List<NatPortMapping>> getPortMappings() {
return new CompletableFuture<>();
}
};
}
}

@ -0,0 +1,117 @@
/*
* 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.nat.manual;
import static org.assertj.core.api.Assertions.assertThat;
import org.hyperledger.besu.nat.core.domain.NatPortMapping;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ExecutionException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class ManualNatManagerTest {
private final String advertisedHost = "99.45.69.12";
private final int p2pPort = 1;
private final int rpcHttpPort = 2;
private ManualNatManager natManager;
@Before
public void initialize() {
natManager = new ManualNatManager(advertisedHost, p2pPort, rpcHttpPort);
natManager.start();
}
@Test
public void assertThatExternalIPIsEqualToRemoteHost()
throws ExecutionException, InterruptedException {
assertThat(natManager.queryExternalIPAddress().get()).isEqualTo(advertisedHost);
}
@Test
public void assertThatLocalIPIsEqualToLocalHost()
throws ExecutionException, InterruptedException, UnknownHostException {
final String internalHost = InetAddress.getLocalHost().getHostAddress();
assertThat(natManager.queryLocalIPAddress().get()).isEqualTo(internalHost);
}
@Test
public void assertThatMappingForDiscoveryWorks() throws UnknownHostException {
final String internalHost = InetAddress.getLocalHost().getHostAddress();
final NatPortMapping mapping =
natManager.getPortMapping(NatServiceType.DISCOVERY, NetworkProtocol.UDP);
final NatPortMapping expectedMapping =
new NatPortMapping(
NatServiceType.DISCOVERY,
NetworkProtocol.UDP,
internalHost,
advertisedHost,
p2pPort,
p2pPort);
assertThat(mapping).isEqualToComparingFieldByField(expectedMapping);
}
@Test
public void assertThatMappingForJsonRpcWorks() throws UnknownHostException {
final String internalHost = InetAddress.getLocalHost().getHostAddress();
final NatPortMapping mapping =
natManager.getPortMapping(NatServiceType.JSON_RPC, NetworkProtocol.TCP);
final NatPortMapping expectedMapping =
new NatPortMapping(
NatServiceType.JSON_RPC,
NetworkProtocol.TCP,
internalHost,
advertisedHost,
rpcHttpPort,
rpcHttpPort);
assertThat(mapping).isEqualToComparingFieldByField(expectedMapping);
}
@Test
public void assertThatMappingForRlpxWorks() throws UnknownHostException {
final String internalHost = InetAddress.getLocalHost().getHostAddress();
final NatPortMapping mapping =
natManager.getPortMapping(NatServiceType.RLPX, NetworkProtocol.TCP);
final NatPortMapping expectedMapping =
new NatPortMapping(
NatServiceType.RLPX,
NetworkProtocol.TCP,
internalHost,
advertisedHost,
p2pPort,
p2pPort);
assertThat(mapping).isEqualToComparingFieldByField(expectedMapping);
}
}

@ -22,6 +22,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.nat.core.domain.NatServiceType;
import org.hyperledger.besu.nat.core.domain.NetworkProtocol;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
@ -104,7 +107,7 @@ public final class UpnpNatManagerTest {
assertThatThrownBy(
() -> {
upnpManager.requestPortForward(80, UpnpNatManager.Protocol.TCP, "");
upnpManager.requestPortForward(80, NetworkProtocol.TCP, NatServiceType.DISCOVERY);
})
.isInstanceOf(IllegalStateException.class);
}
@ -113,7 +116,8 @@ public final class UpnpNatManagerTest {
public void requestPortForwardThrowsWhenPortIsZero() {
upnpManager.start();
assertThatThrownBy(() -> upnpManager.requestPortForward(0, UpnpNatManager.Protocol.TCP, ""))
assertThatThrownBy(
() -> upnpManager.requestPortForward(0, NetworkProtocol.TCP, NatServiceType.DISCOVERY))
.isInstanceOf(IllegalArgumentException.class);
}

Loading…
Cancel
Save