diff --git a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java index a6c832bbe1c..ff65476fde9 100644 --- a/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/RunnerBuilder.java @@ -91,6 +91,8 @@ import org.hyperledger.besu.nat.NatMethod; import org.hyperledger.besu.nat.NatService; import org.hyperledger.besu.nat.core.NatManager; +import org.hyperledger.besu.nat.docker.DockerDetector; +import org.hyperledger.besu.nat.docker.DockerNatManager; import org.hyperledger.besu.nat.manual.ManualNatManager; import org.hyperledger.besu.nat.upnp.UpnpNatManager; import org.hyperledger.besu.plugin.BesuPlugin; @@ -339,7 +341,6 @@ public Runner build() { .orElse(bannedNodes); final NatService natService = new NatService(buildNatManager(natMethod)); - final NetworkBuilder inactiveNetwork = (caps) -> new NoopP2PNetwork(); final NetworkBuilder activeNetwork = (caps) -> @@ -581,13 +582,16 @@ private Optional buildNatManager(final NatMethod natMethod) { final NatMethod detectedNatMethod = Optional.of(natMethod) .filter(not(isEqual(NatMethod.AUTO))) - .orElse(NatService.autoDetectNatMethod()); + .orElse(NatService.autoDetectNatMethod(new DockerDetector())); switch (detectedNatMethod) { case UPNP: return Optional.of(new UpnpNatManager()); case MANUAL: return Optional.of( new ManualNatManager(p2pAdvertisedHost, p2pListenPort, jsonRpcConfiguration.getPort())); + case DOCKER: + return Optional.of( + new DockerNatManager(p2pAdvertisedHost, p2pListenPort, jsonRpcConfiguration.getPort())); case NONE: default: return Optional.empty(); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index 45128ef9751..5883dbc42e3 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -1392,6 +1392,9 @@ public void natMethodOptionIsParsedCorrectly() { parseCommand("--nat-method", "AUTO"); verify(mockRunnerBuilder).natMethod(eq(NatMethod.AUTO)); + parseCommand("--nat-method", "DOCKER"); + verify(mockRunnerBuilder).natMethod(eq(NatMethod.DOCKER)); + assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()).isEmpty(); } @@ -1404,7 +1407,7 @@ public void parsesInvalidNatMethodOptionsShouldFail() { assertThat(commandOutput.toString()).isEmpty(); assertThat(commandErrorOutput.toString()) .contains( - "Invalid value for option '--nat-method': expected one of [UPNP, MANUAL, AUTO, NONE] (case-insensitive) but was 'invalid'"); + "Invalid value for option '--nat-method': expected one of [UPNP, MANUAL, DOCKER, AUTO, NONE] (case-insensitive) but was 'invalid'"); } @Test diff --git a/nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java b/nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java index 5ef21c3daf2..2b72c68feb3 100644 --- a/nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java +++ b/nat/src/main/java/org/hyperledger/besu/nat/NatMethod.java @@ -17,6 +17,7 @@ public enum NatMethod { UPNP, MANUAL, + DOCKER, AUTO, NONE; diff --git a/nat/src/main/java/org/hyperledger/besu/nat/NatService.java b/nat/src/main/java/org/hyperledger/besu/nat/NatService.java index 6f6f469f4f0..8fbbb10a3e9 100644 --- a/nat/src/main/java/org/hyperledger/besu/nat/NatService.java +++ b/nat/src/main/java/org/hyperledger/besu/nat/NatService.java @@ -14,15 +14,13 @@ */ 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.NatMethodDetector; 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.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -185,20 +183,16 @@ private NatMethod retrieveNatMethod(final Optional natManager) { } /** - * Attempts to automatically detect the Nat method being used by the node. + * Attempts to automatically detect the Nat method by applying nat method detectors. Will return + * the first one that succeeds in its detection. * - * @param natMethodAutoDetections list of nat method auto detections + * @param natMethodDetectors 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; + public static NatMethod autoDetectNatMethod(final NatMethodDetector... natMethodDetectors) { + return Arrays.stream(natMethodDetectors) + .flatMap(natMethodDetector -> natMethodDetector.detect().stream()) + .findFirst() + .orElse(NatMethod.NONE); } } diff --git a/nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodAutoDetection.java b/nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodDetector.java similarity index 82% rename from nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodAutoDetection.java rename to nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodDetector.java index f3052a2c981..d39d648d3e0 100644 --- a/nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodAutoDetection.java +++ b/nat/src/main/java/org/hyperledger/besu/nat/core/NatMethodDetector.java @@ -15,8 +15,12 @@ package org.hyperledger.besu.nat.core; +import org.hyperledger.besu.nat.NatMethod; + +import java.util.Optional; + @FunctionalInterface -public interface NatMethodAutoDetection { +public interface NatMethodDetector { - AutoDetectionResult shouldBeThisNatMethod(); + Optional detect(); } diff --git a/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerDetector.java b/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerDetector.java new file mode 100644 index 00000000000..b8ff5ab5f66 --- /dev/null +++ b/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerDetector.java @@ -0,0 +1,40 @@ +/* + * 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.docker; + +import org.hyperledger.besu.nat.NatMethod; +import org.hyperledger.besu.nat.core.NatMethodDetector; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.stream.Stream; + +public class DockerDetector implements NatMethodDetector { + + @Override + public Optional detect() { + try (Stream stream = Files.lines(Paths.get("/proc/1/cgroup"))) { + return stream + .filter(line -> line.contains("/docker")) + .findFirst() + .map(__ -> NatMethod.DOCKER); + } catch (IOException e) { + return Optional.empty(); + } + } +} diff --git a/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerNatManager.java b/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerNatManager.java new file mode 100644 index 00000000000..0c9a97df6a3 --- /dev/null +++ b/nat/src/main/java/org/hyperledger/besu/nat/docker/DockerNatManager.java @@ -0,0 +1,129 @@ +/* + * 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.docker; + +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.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class describes the behaviour of the Docker NAT manager. Docker Nat manager add support for + * Docker’s NAT implementation when Besu is being run from a Docker container + */ +public class DockerNatManager extends AbstractNatManager { + protected static final Logger LOG = LogManager.getLogger(); + + private static final String PORT_MAPPING_TAG = "HOST_PORT_"; + + private final IpDetector ipDetector; + + private final String internalAdvertisedHost; + private final int internalP2pPort; + private final int internalRpcHttpPort; + + private final List forwardedPorts; + + public DockerNatManager(final String advertisedHost, final int p2pPort, final int rpcHttpPort) { + this(new HostBasedIpDetector(), advertisedHost, p2pPort, rpcHttpPort); + } + + public DockerNatManager( + final IpDetector ipDetector, + final String advertisedHost, + final int p2pPort, + final int rpcHttpPort) { + super(NatMethod.DOCKER); + this.ipDetector = ipDetector; + this.internalAdvertisedHost = advertisedHost; + this.internalP2pPort = p2pPort; + this.internalRpcHttpPort = rpcHttpPort; + this.forwardedPorts = buildForwardedPorts(); + } + + private List buildForwardedPorts() { + try { + final String internalHost = queryLocalIPAddress().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + final String advertisedHost = + retrieveExternalIPAddress().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + return Arrays.asList( + new NatPortMapping( + NatServiceType.DISCOVERY, + NetworkProtocol.UDP, + internalHost, + advertisedHost, + internalP2pPort, + getExternalPort(internalP2pPort)), + new NatPortMapping( + NatServiceType.RLPX, + NetworkProtocol.TCP, + internalHost, + advertisedHost, + internalP2pPort, + getExternalPort(internalP2pPort)), + new NatPortMapping( + NatServiceType.JSON_RPC, + NetworkProtocol.TCP, + internalHost, + advertisedHost, + internalRpcHttpPort, + getExternalPort(internalRpcHttpPort))); + } catch (Exception e) { + LOG.warn("Failed to create forwarded port list", e); + } + return Collections.emptyList(); + } + + @Override + protected void doStart() { + LOG.info("Starting docker NAT manager."); + } + + @Override + protected void doStop() { + LOG.info("Stopping docker NAT manager."); + } + + @Override + protected CompletableFuture retrieveExternalIPAddress() { + return ipDetector + .detectExternalIp() + .map(CompletableFuture::completedFuture) + .orElse(CompletableFuture.completedFuture(internalAdvertisedHost)); + } + + @Override + public CompletableFuture> getPortMappings() { + return CompletableFuture.completedFuture(forwardedPorts); + } + + private int getExternalPort(final int defaultValue) { + return Optional.ofNullable(System.getenv(PORT_MAPPING_TAG + defaultValue)) + .map(Integer::valueOf) + .orElse(defaultValue); + } +} diff --git a/nat/src/main/java/org/hyperledger/besu/nat/core/AutoDetectionResult.java b/nat/src/main/java/org/hyperledger/besu/nat/docker/HostBasedIpDetector.java similarity index 55% rename from nat/src/main/java/org/hyperledger/besu/nat/core/AutoDetectionResult.java rename to nat/src/main/java/org/hyperledger/besu/nat/docker/HostBasedIpDetector.java index 22d76384b4b..b84a3d832b3 100644 --- a/nat/src/main/java/org/hyperledger/besu/nat/core/AutoDetectionResult.java +++ b/nat/src/main/java/org/hyperledger/besu/nat/docker/HostBasedIpDetector.java @@ -13,25 +13,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.nat.core; +package org.hyperledger.besu.nat.docker; -import org.hyperledger.besu.nat.NatMethod; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Optional; -public class AutoDetectionResult { +public class HostBasedIpDetector implements IpDetector { - private final NatMethod natMethod; - private final boolean isDetectedNatMethod; + private static final String HOSTNAME = "HOST_IP"; - public AutoDetectionResult(final NatMethod natMethod, final boolean isDetectedNatMethod) { - this.natMethod = natMethod; - this.isDetectedNatMethod = isDetectedNatMethod; - } - - public NatMethod getNatMethod() { - return natMethod; - } - - public boolean isDetectedNatMethod() { - return isDetectedNatMethod; + @Override + public Optional detectExternalIp() { + try { + return Optional.of(InetAddress.getByName(HOSTNAME).getHostAddress()); + } catch (final UnknownHostException e) { + return Optional.empty(); + } } } diff --git a/nat/src/main/java/org/hyperledger/besu/nat/docker/IpDetector.java b/nat/src/main/java/org/hyperledger/besu/nat/docker/IpDetector.java new file mode 100644 index 00000000000..373e1e67e12 --- /dev/null +++ b/nat/src/main/java/org/hyperledger/besu/nat/docker/IpDetector.java @@ -0,0 +1,23 @@ +/* + * 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.docker; + +import java.util.Optional; + +public interface IpDetector { + + Optional detectExternalIp(); +} diff --git a/nat/src/test/java/org/hyperledger/besu/nat/NatServiceTest.java b/nat/src/test/java/org/hyperledger/besu/nat/NatServiceTest.java index efd80d3e688..46735471469 100644 --- a/nat/src/test/java/org/hyperledger/besu/nat/NatServiceTest.java +++ b/nat/src/test/java/org/hyperledger/besu/nat/NatServiceTest.java @@ -20,7 +20,6 @@ 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; @@ -150,23 +149,13 @@ public void assertThatQueryLocalIPAddressWorksProperlyWithoutNat() { @Test public void givenOneAutoDetectionWorksWhenAutoDetectThenReturnCorrectNatMethod() { - final NatMethod natMethod = - NatService.autoDetectNatMethod(NatServiceTest::alwaysTrueShouldBeUpnpMethod); + final NatMethod natMethod = NatService.autoDetectNatMethod(() -> Optional.of(NatMethod.UPNP)); assertThat(natMethod).isEqualTo(NatMethod.UPNP); } @Test public void givenNoAutoDetectionWorksWhenAutoDetectThenReturnEmptyNatMethod() { - final NatMethod natMethod = - NatService.autoDetectNatMethod(NatServiceTest::alwaysFalseShouldBeUpnpMethod); + final NatMethod natMethod = NatService.autoDetectNatMethod(Optional::empty); 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); - } } diff --git a/nat/src/test/java/org/hyperledger/besu/nat/docker/DockerNatManagerTest.java b/nat/src/test/java/org/hyperledger/besu/nat/docker/DockerNatManagerTest.java new file mode 100644 index 00000000000..9a99b8c2b40 --- /dev/null +++ b/nat/src/test/java/org/hyperledger/besu/nat/docker/DockerNatManagerTest.java @@ -0,0 +1,130 @@ +/* + * 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.docker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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.Optional; +import java.util.concurrent.ExecutionException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public final class DockerNatManagerTest { + + private final String advertisedHost = "99.45.69.12"; + private final String detectedAdvertisedHost = "199.45.69.12"; + + private final int p2pPort = 1; + private final int rpcHttpPort = 2; + + @Mock private HostBasedIpDetector hostBasedIpDetector; + + private DockerNatManager natManager; + + @Before + public void initialize() { + hostBasedIpDetector = mock(HostBasedIpDetector.class); + when(hostBasedIpDetector.detectExternalIp()).thenReturn(Optional.of(detectedAdvertisedHost)); + natManager = new DockerNatManager(hostBasedIpDetector, advertisedHost, p2pPort, rpcHttpPort); + natManager.start(); + } + + @Test + public void assertThatExternalIPIsEqualToRemoteHost() + throws ExecutionException, InterruptedException { + assertThat(natManager.queryExternalIPAddress().get()).isEqualTo(detectedAdvertisedHost); + } + + @Test + public void assertThatExternalIPIsEqualToDefaultHostIfIpDetectorCannotRetrieveIP() + throws ExecutionException, InterruptedException { + when(hostBasedIpDetector.detectExternalIp()).thenReturn(Optional.empty()); + 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, + detectedAdvertisedHost, + 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, + detectedAdvertisedHost, + 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, + detectedAdvertisedHost, + p2pPort, + p2pPort); + + assertThat(mapping).isEqualToComparingFieldByField(expectedMapping); + } +}