diff --git a/rskj-core/build.gradle b/rskj-core/build.gradle index 74d14c05db1..1a52fd1f379 100644 --- a/rskj-core/build.gradle +++ b/rskj-core/build.gradle @@ -68,6 +68,7 @@ ext { logbackVersion = '1.2.2' bitcoinjVersion = '0.14.4-rsk-8' nettyVersion = '4.0.56.Final' + weupnpVersion = '0.1.4' } dependencies { @@ -84,6 +85,7 @@ dependencies { compile "org.mapdb:mapdb:2.0-beta13" compile "co.rsk.bitcoinj:bitcoinj-thin:${bitcoinjVersion}" compile 'com.github.briandilley.jsonrpc4j:jsonrpc4j:1.5.1' + compile "org.bitlet:weupnp:${weupnpVersion}" runtime "org.slf4j:log4j-over-slf4j:${slf4jVersion}" runtime "ch.qos.logback:logback-classic:${logbackVersion}" @@ -131,6 +133,9 @@ dependencyVerification { 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', 'org.apache.commons:commons-lang3:8ac96fc686512d777fca85e144f196cd7cfe0c0aec23127229497d1a38ff651c', 'org.awaitility:awaitility:a02982e89585a52c1c84296a895bfeb86ea250cca1a53bcfc8a14092fffa87c4', + // TODO should be updated with hash from reproducible build, once that .jar/.pom is uploaded + // see: https://github.com/rsksmart/reproducible-builds/pull/19 + 'org.bitlet:weupnp:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb', 'org.bouncycastle:bclcrypto-jdk15on:7d03ba37df4d0ddc4ea40d56554324c6f18062a930edadb0a1b3acbbbea28efc', 'org.ethereum:leveldbjni-all:18da00444c77080d4422b16c9d4750c4addabda350b702b4a6d628b86658e585', 'org.fusesource.hawtjni:hawtjni-runtime:74fe9764e1fb1ef20b159dbca2d29abd6de292082ce3fcf538f81ac912390416', diff --git a/rskj-core/src/main/java/co/rsk/RskContext.java b/rskj-core/src/main/java/co/rsk/RskContext.java index c4c4a0013c0..1a53df9112b 100644 --- a/rskj-core/src/main/java/co/rsk/RskContext.java +++ b/rskj-core/src/main/java/co/rsk/RskContext.java @@ -49,6 +49,7 @@ import co.rsk.net.eth.WriterMessageRecorder; import co.rsk.net.sync.PeersInformation; import co.rsk.net.sync.SyncConfiguration; +import co.rsk.net.discovery.upnp.UpnpService; import co.rsk.peg.BridgeSupportFactory; import co.rsk.peg.BtcBlockStoreWithCache; import co.rsk.peg.RepositoryBtcBlockStoreWithCache; @@ -234,6 +235,7 @@ public class RskContext implements NodeBootstrapper { private StatusResolver statusResolver; private Web3InformationRetriever web3InformationRetriever; private BootstrapImporter bootstrapImporter; + private UpnpService upnpService; public RskContext(String[] args) { this(new CliArgs.Parser<>( @@ -780,10 +782,15 @@ public List buildInternalServices() { internalServices.add(getWeb3WebSocketServer()); } if (getRskSystemProperties().isPeerDiscoveryEnabled()) { + boolean isUpnpEnabled = getRskSystemProperties().isPeerDiscoveryByUpnpEnabled(); + if (isUpnpEnabled) { + internalServices.add(getUpnpService()); + } internalServices.add(new UDPServer( getRskSystemProperties().getBindAddress().getHostAddress(), getRskSystemProperties().getPeerPort(), - getPeerExplorer() + getPeerExplorer(), + isUpnpEnabled ? Optional.of(getUpnpService()) : Optional.empty() )); } if (getRskSystemProperties().isSyncEnabled()) { @@ -1607,6 +1614,13 @@ private MinerClock getMinerClock() { return minerClock; } + private UpnpService getUpnpService() { + if (upnpService == null) { + upnpService = new UpnpService(); + } + return upnpService; + } + public org.ethereum.db.BlockStore buildBlockStore(String databaseDir) { File blockIndexDirectory = new File(databaseDir + "/blocks/"); File dbFile = new File(blockIndexDirectory, "index"); diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/UDPServer.java b/rskj-core/src/main/java/co/rsk/net/discovery/UDPServer.java index 8002199e01b..9e07aa933e7 100644 --- a/rskj-core/src/main/java/co/rsk/net/discovery/UDPServer.java +++ b/rskj-core/src/main/java/co/rsk/net/discovery/UDPServer.java @@ -19,6 +19,8 @@ package co.rsk.net.discovery; import co.rsk.config.InternalService; +import co.rsk.net.discovery.upnp.UpnpProtocol; +import co.rsk.net.discovery.upnp.UpnpService; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; @@ -28,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Optional; import java.util.concurrent.TimeUnit; /** @@ -35,6 +38,7 @@ */ public class UDPServer implements InternalService { private static final Logger logger = LoggerFactory.getLogger(UDPServer.class); + private static final String PEER_DISCOVERY_PORT_MAPPING_DESCRIPTION = "RSK peer discovery"; private int port; private String address; @@ -43,11 +47,17 @@ public class UDPServer implements InternalService { private volatile boolean shutdown = false; private PeerExplorer peerExplorer; + private final Optional upnpService; public UDPServer(String address, int port, PeerExplorer peerExplorer) { + this(address, port, peerExplorer, Optional.empty()); + } + + public UDPServer(String address, int port, PeerExplorer peerExplorer, Optional upnpService) { this.address = address; this.port = port; this.peerExplorer = peerExplorer; + this.upnpService = upnpService; } @Override @@ -59,6 +69,7 @@ public void start() { @Override public void run() { try { + UDPServer.this.doPortMappingIfEnabled(); UDPServer.this.startUDPServer(); } catch (Exception e) { logger.error("Discovery can't be started. ", e); @@ -86,6 +97,17 @@ public void startUDPServer() throws InterruptedException { group.shutdownGracefully().sync(); } + private void doPortMappingIfEnabled() { + upnpService + .flatMap(service -> service.findGateway(address)) + .ifPresent(gateway -> gateway.addPortMapping( + port, + port, + UpnpProtocol.UDP, + PEER_DISCOVERY_PORT_MAPPING_DESCRIPTION + )); + } + @Override public void stop() { logger.info("Closing UDPListener..."); diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpGatewayManager.java b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpGatewayManager.java new file mode 100644 index 00000000000..94da06acf65 --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpGatewayManager.java @@ -0,0 +1,206 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.net.discovery.upnp; + +import org.bitlet.weupnp.GatewayDevice; +import org.bitlet.weupnp.PortMappingEntry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.Optional; + +/** + * Provides a UPnP interface for a particular Internet Gateway Device: + *
    + *
  • Add/remove port mappings.
  • + *
  • Find the gateway's external IP address.
  • + *
+ */ +public class UpnpGatewayManager { + + private static final Logger logger = LoggerFactory.getLogger(UpnpGatewayManager.class); + private static final int PORT_MAPPINGS_INITIAL_CAPACITY = 3; + + private final GatewayDevice gateway; + private final List portMappings = new ArrayList<>(PORT_MAPPINGS_INITIAL_CAPACITY); + + /** + * Package-private constructor. Called by {@link UpnpService#findGateway()}. + * + * @param gateway a valid gateway with a WAN connection. + */ + UpnpGatewayManager(GatewayDevice gateway) { + this.gateway = gateway; + } + + /** + * Gets the external IP address of the gateway. + * + * @return the external IP address of the gateway, or empty if failure. + */ + public Optional getExternalIPAddress() { + try { + return Optional.ofNullable(gateway.getExternalIPAddress()); + } catch (Exception e) { + logger.error("Failed to get external IP address.", e); + return Optional.empty(); + } + } + + /** + * Forwards a port on the gateway to this node. The port mapping will be deleted + * on {@link UpnpService#stop()}. + * + * @param externalPort the external port to be forwarded. + * @param internalPort the destination port being forwarded to. + * @param protocol the protocol to use. + * @param description describes the purpose of this port mapping. + * @return true if successful. + */ + public boolean addPortMapping(int externalPort, int internalPort, UpnpProtocol protocol, String description) { + String strProtocol = protocol.toString(); + String strLocalAddress = gateway.getLocalAddress().getHostAddress(); + + if (addPortMapping(externalPort, internalPort, strLocalAddress, strProtocol, description)) { + logger.info( + "Added port mapping of {} port {} to {}:{} for \"{}\".", + strProtocol, + externalPort, + strLocalAddress, + internalPort, + description + ); + // saved here for release on service shutdown + portMappings.add(createPortMappingEntryObject( + strProtocol, + externalPort, + strLocalAddress, + internalPort, + description + )); + return true; + } else { + logger.error(getPortForwardingExceptionMessage( + strProtocol, + externalPort, + strLocalAddress, + internalPort, + description + )); + return false; + } + } + + private boolean addPortMapping( + int externalPort, + int internalPort, + String strLocalAddress, + String strProtocol, + String description) { + + try { + return gateway.addPortMapping( + externalPort, + internalPort, + strLocalAddress, + strProtocol, + description + ); + } catch (Exception e) { + logger.error(getPortForwardingExceptionMessage( + strProtocol, + externalPort, + strLocalAddress, + internalPort, + description + ), e); + return false; + } + } + + private static PortMappingEntry createPortMappingEntryObject( + String protocol, + int externalPort, + String localAddress, + int internalPort, + String description) { + + PortMappingEntry pm = new PortMappingEntry(); + pm.setProtocol(protocol); + pm.setExternalPort(externalPort); + pm.setInternalClient(localAddress); + pm.setInternalPort(internalPort); + pm.setPortMappingDescription(description); + return pm; + } + + private static String getPortForwardingExceptionMessage( + String strProtocol, + int externalPort, + String strLocalAddress, + int internalPort, + String description) { + return String.format( + "Failed to forward %s port %s to %s:%s for %s.", + strProtocol, + externalPort, + strLocalAddress, + internalPort, + description + ); + } + + /** + * Deletes all port mappings which were created by calls to + * {@link #addPortMapping(int, int, UpnpProtocol, String)}. + */ + public void deleteAllPortMappings() { + ListIterator iter = portMappings.listIterator(); + while (iter.hasNext()) { + PortMappingEntry entry = iter.next(); + int externalPort = entry.getExternalPort(); + String protocol = entry.getProtocol(); + + if (deletePortMapping(externalPort, protocol)) { + logger.info( + "Deleted port mapping of {} port {}.", + protocol, + externalPort + ); + iter.remove(); + } + } + } + + private boolean deletePortMapping(int externalPort, String protocol) { + try { + return gateway.deletePortMapping(externalPort, protocol); + } catch (Exception e) { + logger.error(String.format( + "Failed to delete port mapping of %s port %s", + protocol, + externalPort + ), e); + return false; + } + } +} diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpProtocol.java b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpProtocol.java new file mode 100644 index 00000000000..f405bce924d --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpProtocol.java @@ -0,0 +1,23 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.net.discovery.upnp; + +public enum UpnpProtocol { + TCP, UDP +} diff --git a/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpService.java b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpService.java new file mode 100644 index 00000000000..96ca3a4a8ab --- /dev/null +++ b/rskj-core/src/main/java/co/rsk/net/discovery/upnp/UpnpService.java @@ -0,0 +1,209 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.net.discovery.upnp; + +import co.rsk.config.InternalService; +import org.bitlet.weupnp.GatewayDevice; +import org.bitlet.weupnp.GatewayDiscover; +import org.ethereum.config.SystemProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Can query the local network interfaces for UPnP-enabled gateways, and provides an + * {@link UpnpGatewayManager abstraction} for interacting with those gateways. + */ +public class UpnpService implements InternalService { + + private static final Logger logger = LoggerFactory.getLogger(UpnpService.class); + private static final String DEVICE_SEARCH_WANIPCONNECTION = "urn:schemas-upnp-org:service:WANIPConnection:1"; + private static final String UPNP_DISABLED_NOTIFICATION = "Ensure that your router has UPnP enabled, " + + "or disable UPnP in the settings of this node (" + SystemProperties.PROPERTY_PEER_DISCOVERY_UPNP_ENABLED + + ") and forward ports manually."; + private static final int DEFAULT_UPNP_TIMEOUT_MILLIS = 5000; + private static final int DELETE_PORT_MAPPINGS_TIMEOUT_MILLIS = 5000; + private static final int CACHE_INITIAL_CAPACITY = 4; + private static final InetAddress localWildcardAddress = new InetSocketAddress(0).getAddress(); + + private final GatewayDiscover query; + private final Map cachedGatewayManagers = + new HashMap<>(CACHE_INITIAL_CAPACITY); + + /** + * Default constructor.
+ * Will only find gateways which have {@link #DEVICE_SEARCH_WANIPCONNECTION a valid WAN connection}, + * and uses a UPnP timeout of {@value #DEFAULT_UPNP_TIMEOUT_MILLIS} milliseconds. + */ + public UpnpService() { + this(new GatewayDiscover(new String[]{DEVICE_SEARCH_WANIPCONNECTION}), DEFAULT_UPNP_TIMEOUT_MILLIS); + } + + /** + * Constructor. + * + * @param query The gateway discoverer to use. + * @param timeout UPnP messaging timeout, in milliseconds. + */ + public UpnpService(GatewayDiscover query, int timeout) { + this.query = query; + // only available as a static call, but UpnpService is a singleton so it should be fine + GatewayDevice.setHttpReadTimeout(timeout); + } + + @Override + public void start() { + // empty; device discovery is lazy + } + + @Override + public void stop() { + logger.info("Deleting port mappings..."); + Collection gateways = cachedGatewayManagers.values(); + if (!gateways.isEmpty()) { + // release port mappings of each gateway concurrently + ExecutorService executor = Executors.newFixedThreadPool(gateways.size()); + try { + executor.invokeAll( + gateways.stream() + .map(gateway -> Executors.callable(gateway::deleteAllPortMappings)) + .collect(Collectors.toList()), + DELETE_PORT_MAPPINGS_TIMEOUT_MILLIS, + TimeUnit.MILLISECONDS + ); + } catch (Exception e) { + // best effort was made; continue with service shutdown + logger.warn("Exception caught while waiting for port mappings to be released; ignoring."); + } finally { + executor.shutdown(); + } + } + cachedGatewayManagers.clear(); + } + + /** + * Searches for a gateway on the network(s) that this machine is connected to, + * and returns the first valid one found. + *
+ * This is equivalent to calling {@link #findGateway(InetAddress)} with a wildcard address. + * + * @return a UPnP interface for the gateway, or empty if none found. + */ + public synchronized Optional findGateway() { + return findGateway(localWildcardAddress); + } + + /** + * Searches for a gateway connected to the given local address. + * + * @param localAddress the local address used to find the gateway. Use a wildcard address to + * search all local addresses. + * @return a UPnP interface for the gateway, or empty if none found. + */ + public synchronized Optional findGateway(String localAddress) { + try { + return findGateway(InetAddress.getByName(localAddress)); + } catch (UnknownHostException e) { + logger.error("Unable to resolve local address.", e); + return Optional.empty(); + } + } + + /** + * Searches for a gateway connected to the given local address. + * + * @param localAddress the local address used to find the gateway. Use a wildcard address to + * search all local addresses. + * @return a UPnP interface for the gateway, or empty if none found. + */ + public synchronized Optional findGateway(InetAddress localAddress) { + // check cached results to avoid redundant queries + UpnpGatewayManager gatewayManager = cachedGatewayManagers.get(localAddress); + if (gatewayManager != null) { + return Optional.of(gatewayManager); + } + + GatewayDevice gateway; + if (localAddress.isAnyLocalAddress()) { + logger.info("Searching all local interfaces for a valid gateway..."); + searchForAllGateways(); + // get the first valid gateway found from the search results + gateway = query.getValidGateway(); + if (gateway != null) { + logger.info( + "Found gateway \"{}\", connected at local address {}.", + gateway.getFriendlyName(), + gateway.getLocalAddress().getHostAddress() + ); + } + } else { + logger.info("Searching {} for a valid gateway...", localAddress); + gateway = searchForAllGateways().get(localAddress); + if (gateway != null) { + if (isValidGateway(gateway)) { + logger.info( + "Found gateway \"{}\", connected at local address {}.", + gateway.getFriendlyName(), + localAddress.getHostAddress() + ); + } else { + gateway = null; + } + } + } + + if (gateway == null) { + logger.error("No valid gateway found. " + UPNP_DISABLED_NOTIFICATION); + } else { + gatewayManager = new UpnpGatewayManager(gateway); + cachedGatewayManagers.put(localAddress, gatewayManager); + } + return Optional.ofNullable(gatewayManager); + } + + private synchronized Map searchForAllGateways() { + try { + return query.discover(); + } catch (Exception e) { + logger.error("Gateway discovery failed.", e); + return Collections.emptyMap(); + } + } + + private synchronized boolean isValidGateway(GatewayDevice gateway) { + try { + return gateway.isConnected(); + } catch (Exception e) { + logger.error(String.format( + "Failed to read status of gateway \"%s\" connected at local address %s.", + gateway.getFriendlyName(), + gateway.getLocalAddress().getHostAddress() + ), e); + return false; + } + } +} diff --git a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java index b143f51265d..89891192d82 100644 --- a/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java +++ b/rskj-core/src/main/java/org/ethereum/config/SystemProperties.java @@ -83,6 +83,7 @@ public abstract class SystemProperties { public static final String PROPERTY_PUBLIC_IP = "public.ip"; public static final String PROPERTY_BIND_ADDRESS = "bind_address"; + public static final String PROPERTY_PEER_DISCOVERY_UPNP_ENABLED = "peer.discovery.upnp_enabled"; /* Testing */ private static final Boolean DEFAULT_VMTEST_LOAD_LOCAL = false; @@ -195,6 +196,10 @@ public List peerDiscoveryIPList() { return configFromFiles.hasPath("peer.discovery.ip.list") ? configFromFiles.getStringList("peer.discovery.ip.list") : new ArrayList<>(); } + public boolean isPeerDiscoveryByUpnpEnabled() { + return configFromFiles.getBoolean(PROPERTY_PEER_DISCOVERY_UPNP_ENABLED); + } + public boolean databaseReset() { return configFromFiles.getBoolean("database.reset"); } diff --git a/rskj-core/src/main/resources/config/main.conf b/rskj-core/src/main/resources/config/main.conf index 0d775818408..f0184c8fa46 100644 --- a/rskj-core/src/main/resources/config/main.conf +++ b/rskj-core/src/main/resources/config/main.conf @@ -19,6 +19,10 @@ peer { # peer [true/false] enabled = true + # Use UPnP to manage port forwarding on NAT router. + # Router must have UPnP enabled. + upnp_enabled = true + # List of the peers to start # the search of the online peers # values: [ip:port] diff --git a/rskj-core/src/main/resources/config/regtest.conf b/rskj-core/src/main/resources/config/regtest.conf index a00a5a9c0a8..324b408a8a6 100644 --- a/rskj-core/src/main/resources/config/regtest.conf +++ b/rskj-core/src/main/resources/config/regtest.conf @@ -22,6 +22,10 @@ peer { # peer [true/false] enabled = false + # Use UPnP to manage port forwarding on NAT router. + # Router must have UPnP enabled. + upnp_enabled = false + # List of the peers to start # the search of the online peers # values: [ip:port] diff --git a/rskj-core/src/main/resources/config/testnet.conf b/rskj-core/src/main/resources/config/testnet.conf index 117cf76f756..258071d83cb 100644 --- a/rskj-core/src/main/resources/config/testnet.conf +++ b/rskj-core/src/main/resources/config/testnet.conf @@ -23,6 +23,10 @@ peer { # peer [true/false] enabled = true + # Use UPnP to manage port forwarding on NAT router. + # Router must have UPnP enabled. + upnp_enabled = true + # List of the peers to start # the search of the online peers # values: [ip:port] diff --git a/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java b/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java index 607db191ec8..259fd5f473f 100644 --- a/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java +++ b/rskj-core/src/test/java/co/rsk/config/RskSystemPropertiesTest.java @@ -18,6 +18,7 @@ package co.rsk.config; +import com.typesafe.config.ConfigException; import org.junit.Assert; import org.junit.Test; @@ -49,4 +50,13 @@ public void hasMessagesConfiguredInTestConfig() { Assert.assertTrue(commands.contains("TRANSACTIONS")); Assert.assertTrue(commands.contains("RSK_MESSAGE:BLOCK_MESSAGE")); } + + @Test + public void isOptionConfiguredInTestConfig() { + try { + config.isPeerDiscoveryByUpnpEnabled(); + } catch (ConfigException.Missing e) { + Assert.fail(e.getMessage()); + } + } } diff --git a/rskj-core/src/test/java/co/rsk/db/importer/BootstrapImporterTest.java b/rskj-core/src/test/java/co/rsk/db/importer/BootstrapImporterTest.java index ae7ab0a97ee..685b2d65854 100644 --- a/rskj-core/src/test/java/co/rsk/db/importer/BootstrapImporterTest.java +++ b/rskj-core/src/test/java/co/rsk/db/importer/BootstrapImporterTest.java @@ -7,6 +7,7 @@ import org.junit.Test; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -17,7 +18,7 @@ public class BootstrapImporterTest { @Test - public void importData() throws IOException { + public void importData() throws IOException, URISyntaxException { BlockStore blockStore = mock(BlockStore.class); when(blockStore.getMaxNumber()).thenReturn(0L); when(blockStore.isEmpty()).thenReturn(false); @@ -26,7 +27,7 @@ public void importData() throws IOException { TrieStore trieStore = mock(TrieStore.class); BootstrapDataProvider bootstrapDataProvider = mock(BootstrapDataProvider.class); - Path path = Paths.get(getClass().getClassLoader().getResource("import/bootstrap-data.bin").getPath()); + Path path = Paths.get(getClass().getClassLoader().getResource("import/bootstrap-data.bin").toURI()); byte[] oneBlockAndState = Files.readAllBytes(path); when(bootstrapDataProvider.getBootstrapData()).thenReturn(oneBlockAndState); when(bootstrapDataProvider.getSelectedHeight()).thenReturn(1L); diff --git a/rskj-core/src/test/java/co/rsk/net/discovery/UDPServerTest.java b/rskj-core/src/test/java/co/rsk/net/discovery/UDPServerTest.java index c347d012016..bd1e34c6fb5 100644 --- a/rskj-core/src/test/java/co/rsk/net/discovery/UDPServerTest.java +++ b/rskj-core/src/test/java/co/rsk/net/discovery/UDPServerTest.java @@ -20,6 +20,9 @@ import co.rsk.net.discovery.table.KademliaOptions; import co.rsk.net.discovery.table.NodeDistanceTable; +import co.rsk.net.discovery.upnp.UpnpGatewayManager; +import co.rsk.net.discovery.upnp.UpnpProtocol; +import co.rsk.net.discovery.upnp.UpnpService; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.encoders.Hex; import org.ethereum.crypto.ECKey; @@ -27,12 +30,16 @@ import org.ethereum.net.server.Channel; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.powermock.reflect.Whitebox; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; +import static org.mockito.Mockito.*; + /** * Created by mario on 15/02/17. */ @@ -57,6 +64,8 @@ public class UDPServerTest { private static final long UPDATE = 60000; private static final long CLEAN = 60000; + private static final long START_THREAD_TIMEOUT = 10000; + @Test public void port0DoesntCreateANewChannel() throws InterruptedException { UDPServer udpServer = new UDPServer(HOST, 0, null); @@ -157,4 +166,40 @@ private boolean checkNodeIds(List nodes, String... idsToCheck) { } return check; } + + @Test + public void testUpnpPortMapping() throws Exception { + // init mocks + UpnpService mockUpnpService = mock(UpnpService.class); + UpnpGatewayManager mockGatewayManager = mock(UpnpGatewayManager.class); + PeerExplorer mockPeerExplorer = mock(PeerExplorer.class); + + // stub mocks + when(mockUpnpService.findGateway(anyString())) + .thenReturn(Optional.of(mockGatewayManager)); + when(mockGatewayManager.addPortMapping(anyInt(), anyInt(), any(UpnpProtocol.class), anyString())) + .thenReturn(true); + + // start UDPServer, but prevent it from opening an actual channel + UDPServer testServer = spy(new UDPServer(HOST, PORT_1, mockPeerExplorer, Optional.of(mockUpnpService))); + doNothing().when(testServer).startUDPServer(); + testServer.start(); + + // verify that correct values were used for port forwarding + ArgumentCaptor acAddress = ArgumentCaptor.forClass(String.class); + ArgumentCaptor acExternalPort = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor acInternalPort = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor acProtocol = ArgumentCaptor.forClass(UpnpProtocol.class); + + // wait for UDPServer::start()'s thread to make the calls + verify(mockUpnpService, timeout(START_THREAD_TIMEOUT)).findGateway(acAddress.capture()); + verify(mockGatewayManager, timeout(START_THREAD_TIMEOUT)) + .addPortMapping(acExternalPort.capture(), acInternalPort.capture(), acProtocol.capture(), anyString()); + + String errorTemplate = "Wrong %s used for port mapping."; + Assert.assertEquals(String.format(errorTemplate, "address"), HOST, acAddress.getValue()); + Assert.assertEquals(String.format(errorTemplate, "external port"), PORT_1, acExternalPort.getValue().intValue()); + Assert.assertEquals(String.format(errorTemplate, "internal port"), PORT_1, acInternalPort.getValue().intValue()); + Assert.assertEquals(String.format(errorTemplate, "protocol"), UpnpProtocol.UDP, acProtocol.getValue()); + } } diff --git a/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpGatewayManagerTest.java b/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpGatewayManagerTest.java new file mode 100644 index 00000000000..326d8ff9af8 --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpGatewayManagerTest.java @@ -0,0 +1,185 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package co.rsk.net.discovery.upnp; + +import com.google.common.collect.ImmutableMap; +import org.bitlet.weupnp.GatewayDevice; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; + +import static org.mockito.Mockito.*; + +public class UpnpGatewayManagerTest { + + // test data + private static final int PORT_1 = 5000; + private static final int PORT_2 = 6000; + private static final String DESCRIPTION = "Unit test port mapping"; + private static final String EXTERNAL_IP_ADDRESS = "255.255.255.200"; + private static final InetAddress localAddress = Inet4Address.getLoopbackAddress(); + + // mock objects + private GatewayDevice mockGatewayBad; + private GatewayDevice mockGatewayGood; + private GatewayDevice mockGatewayGetExternalIpException; + private GatewayDevice mockGatewayAddPortMappingException; + + @Before + public void initialize() throws Exception { + // initialize mocks + mockGatewayBad = mock(GatewayDevice.class); + mockGatewayBad = mock(GatewayDevice.class); + mockGatewayGood = mock(GatewayDevice.class); + mockGatewayGetExternalIpException = mock(GatewayDevice.class); + mockGatewayAddPortMappingException = mock(GatewayDevice.class); + + // setup mock stubs + when(mockGatewayBad.getLocalAddress()).thenReturn(localAddress); + when(mockGatewayBad.getExternalIPAddress()).thenReturn(null); + when(mockGatewayBad.addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(false); + when(mockGatewayBad.deletePortMapping(anyInt(), anyString())).thenReturn(false); + + when(mockGatewayGood.getLocalAddress()).thenReturn(localAddress); + when(mockGatewayGood.getExternalIPAddress()).thenReturn(EXTERNAL_IP_ADDRESS); + when(mockGatewayGood.addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(true); + when(mockGatewayGood.deletePortMapping(anyInt(), anyString())).thenReturn(true); + + when(mockGatewayGetExternalIpException.getLocalAddress()).thenReturn(localAddress); + when(mockGatewayGetExternalIpException.getExternalIPAddress()).thenThrow(Exception.class); + + when(mockGatewayAddPortMappingException.getLocalAddress()).thenReturn(localAddress); + when(mockGatewayAddPortMappingException.addPortMapping( + anyInt(), + anyInt(), + anyString(), + anyString(), + anyString() + )).thenThrow(Exception.class); + } + + @Test + public void testGetExternalIpAddress() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayGood); + Assert.assertTrue( + "Expected non-empty response when gateway successfully returns the external IP address.", + testManager.getExternalIPAddress().isPresent() + ); + } + + @Test + public void testGetExternalIpAddressEmpty() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayBad); + Assert.assertFalse( + "Expected empty response when gateway fails to return the external IP address.", + testManager.getExternalIPAddress().isPresent() + ); + } + + @Test + public void testGetExternalIpAddressExceptional() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayGetExternalIpException); + Assert.assertFalse( + "Expected empty response when gateway throws Exception.", + testManager.getExternalIPAddress().isPresent() + ); + } + + @Test + public void testAddPortMapping() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayGood); + Assert.assertTrue( + "Failed to return true when gateway successfully added a port mapping.", + testManager.addPortMapping(PORT_1, PORT_1, UpnpProtocol.UDP, DESCRIPTION) + ); + } + + @Test + public void testAddPortMappingFail() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayBad); + Assert.assertFalse( + "Failed to return false for an invalid port mapping.", + testManager.addPortMapping(PORT_1, PORT_1, UpnpProtocol.UDP, DESCRIPTION) + ); + } + + @Test + public void testAddPortMappingException() { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayAddPortMappingException); + Assert.assertFalse( + "Failed to return false when gateway throws Exception", + testManager.addPortMapping(PORT_1, PORT_1, UpnpProtocol.TCP, DESCRIPTION) + ); + } + + @Test + public void testDeleteAllPortMappings() throws IOException, SAXException { + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayGood); + // ordered map + ImmutableMap portMappings = ImmutableMap.of( + PORT_1, UpnpProtocol.UDP, + PORT_2, UpnpProtocol.TCP + ); + + // add some ports then remove them all + portMappings.forEach((port, protocol) -> testManager.addPortMapping(port, port, protocol, DESCRIPTION)); + testManager.deleteAllPortMappings(); + + // verify that correct ports were deleted + ArgumentCaptor acExternalPorts = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor acProtocols = ArgumentCaptor.forClass(String.class); + verify(mockGatewayGood, times(portMappings.size())).deletePortMapping( + acExternalPorts.capture(), + acProtocols.capture() + ); + + String errorTemplate = "Wrong port %s removed."; + Assert.assertArrayEquals( + String.format(errorTemplate, "numbers"), + portMappings.keySet().toArray(), + acExternalPorts.getAllValues().toArray() + ); + Assert.assertArrayEquals( + String.format(errorTemplate, "protocols"), + portMappings.values().stream().map(Object::toString).toArray(), + acProtocols.getAllValues().toArray() + ); + } + + @Test + public void testDeleteAllPortMappingsException() throws IOException, SAXException { + doThrow(Exception.class).when(mockGatewayGood).deletePortMapping(anyInt(), anyString()); + + UpnpGatewayManager testManager = new UpnpGatewayManager(mockGatewayGood); + testManager.addPortMapping(PORT_1, PORT_1, UpnpProtocol.UDP, DESCRIPTION); + try { + testManager.deleteAllPortMappings(); + } catch (Exception e) { + Assert.fail("Deletion failure should not result in a thrown Exception."); + } + } +} diff --git a/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpServiceTest.java b/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpServiceTest.java new file mode 100644 index 00000000000..ff80f3b1f9e --- /dev/null +++ b/rskj-core/src/test/java/co/rsk/net/discovery/upnp/UpnpServiceTest.java @@ -0,0 +1,316 @@ +/* + * This file is part of RskJ + * Copyright (C) 2019 RSK Labs Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package co.rsk.net.discovery.upnp; + +import com.google.common.collect.ImmutableMap; +import org.bitlet.weupnp.GatewayDevice; +import org.bitlet.weupnp.GatewayDiscover; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Optional; + +import static org.mockito.Mockito.*; + +public final class UpnpServiceTest { + + // test data + private static final String LOCATION_1 = "192.168.0.1"; + private static final String LOCATION_2 = "192.168.1.1"; + private static final int PORT_1 = 5000; + private static final int PORT_2 = 6000; + private static final String LOCAL_ADDRESS_VALID_1 = "192.168.0.100"; + private static final String LOCAL_ADDRESS_VALID_2 = "192.168.1.100"; + private static final String LOCAL_ADDRESS_INVALID = "192.168.0.101"; + private static final String VALID_GATEWAY_NAME_1 = "Valid Mock Gateway 1"; + private static final String VALID_GATEWAY_NAME_2 = "Valid Mock Gateway 2"; + private static final String INVALID_GATEWAY_NAME = "Invalid Mock Gateway"; + private static final String PORT_MAPPING_DESCRIPTION_1 = "Unit test port mapping"; + private static final String PORT_MAPPING_DESCRIPTION_2 = "Unit test port mapping 2"; + private static final int DEFAULT_UPNP_TIMEOUT = 5000; + private static final long STOP_UPNP_SERVICE_TIMEOUT = 10000; + private static final InetAddress wildcardAddress = new InetSocketAddress(0).getAddress(); + + private InetAddress localAddressValid1; + private InetAddress localAddressValid2; + private InetAddress localAddressInvalid; + + // mock objects + private GatewayDevice mockGatewayValid1; + private GatewayDevice mockGatewayValid2; + private GatewayDevice mockGatewayInvalid; + private GatewayDiscover mockWildcardDiscoverValid; + private GatewayDiscover mockWildcardDiscoverInvalid; + private GatewayDiscover mockWildcardDiscoverNone; + private GatewayDiscover mockWildcardDiscoverExceptional; + private GatewayDiscover mockSpecificDiscoverValid; + private GatewayDiscover mockSpecificDiscoverValidMultiple; + private GatewayDiscover mockSpecificDiscoverInvalid; + private GatewayDiscover mockSpecificDiscoverNone; + + public UpnpServiceTest() throws Exception { + try { + // just parses the IPs; no hostname lookup + this.localAddressValid1 = InetAddress.getByName(LOCAL_ADDRESS_VALID_1); + this.localAddressValid2 = InetAddress.getByName(LOCAL_ADDRESS_VALID_2); + this.localAddressInvalid = InetAddress.getByName(LOCAL_ADDRESS_INVALID); + } catch (UnknownHostException e) { + // this should never happen + throw new Exception("Failed to parse IP address.", e); + } + } + + @Before + public void initialize() throws Exception { + // initialize mocks + mockGatewayValid1 = mock(GatewayDevice.class); + mockGatewayValid2 = mock(GatewayDevice.class); + mockGatewayInvalid = mock(GatewayDevice.class); + mockWildcardDiscoverValid = mock(GatewayDiscover.class); + mockWildcardDiscoverInvalid = mock(GatewayDiscover.class); + mockWildcardDiscoverNone = mock(GatewayDiscover.class); + mockWildcardDiscoverExceptional = mock(GatewayDiscover.class); + mockSpecificDiscoverValid = mock(GatewayDiscover.class); + mockSpecificDiscoverValidMultiple = mock(GatewayDiscover.class); + mockSpecificDiscoverInvalid = mock(GatewayDiscover.class); + mockSpecificDiscoverNone = mock(GatewayDiscover.class); + + // setup mock stubs + when(mockGatewayValid1.isConnected()).thenReturn(true); + when(mockGatewayValid1.getLocalAddress()).thenReturn(localAddressValid1); + when(mockGatewayValid1.getFriendlyName()).thenReturn(VALID_GATEWAY_NAME_1); + when(mockGatewayValid1.addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(true); + when(mockGatewayValid1.deletePortMapping(anyInt(), anyString())).thenReturn(true); + when(mockGatewayValid1.getLocation()).thenReturn(LOCATION_1); + + when(mockGatewayValid2.isConnected()).thenReturn(true); + when(mockGatewayValid2.getLocalAddress()).thenReturn(localAddressValid2); + when(mockGatewayValid2.getFriendlyName()).thenReturn(VALID_GATEWAY_NAME_2); + when(mockGatewayValid2.addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(true); + when(mockGatewayValid2.deletePortMapping(anyInt(), anyString())).thenReturn(true); + when(mockGatewayValid2.getLocation()).thenReturn(LOCATION_2); + + when(mockGatewayInvalid.isConnected()).thenReturn(false); + when(mockGatewayInvalid.getLocalAddress()).thenReturn(localAddressInvalid); + when(mockGatewayInvalid.getFriendlyName()).thenReturn(INVALID_GATEWAY_NAME); + when(mockGatewayInvalid.getLocation()).thenReturn(LOCATION_1); + + when(mockWildcardDiscoverValid.discover()).thenReturn(ImmutableMap.of( + localAddressValid1, mockGatewayValid1, + localAddressInvalid, mockGatewayInvalid + )); + when(mockWildcardDiscoverValid.getValidGateway()).thenReturn(mockGatewayValid1); + + when(mockWildcardDiscoverInvalid.discover()).thenReturn(ImmutableMap.of( + localAddressValid1, mockGatewayValid1 + )); + when(mockWildcardDiscoverInvalid.getValidGateway()).thenReturn(null); + + when(mockWildcardDiscoverNone.discover()).thenReturn(ImmutableMap.of()); + when(mockWildcardDiscoverNone.getValidGateway()).thenReturn(null); + + when(mockWildcardDiscoverExceptional.discover()).thenThrow(Exception.class); + + when(mockSpecificDiscoverValid.discover()).thenReturn(ImmutableMap.of( + localAddressValid1, mockGatewayValid1 + )); + + when(mockSpecificDiscoverValidMultiple.discover()).thenReturn(ImmutableMap.of( + localAddressValid1, mockGatewayValid1, + localAddressValid2, mockGatewayValid2 + )); + + when(mockSpecificDiscoverInvalid.discover()).thenReturn(ImmutableMap.of( + localAddressInvalid, mockGatewayInvalid + )); + + when(mockSpecificDiscoverNone.discover()).thenReturn(ImmutableMap.of()); + } + + private static UpnpService createAndStartUpnpService(GatewayDiscover query) { + UpnpService upnpService = new UpnpService(query, DEFAULT_UPNP_TIMEOUT); + upnpService.start(); + return upnpService; + } + + @Test + public void testStopWithoutValidSearchResults() { + try { + createAndStartUpnpService(mockWildcardDiscoverInvalid).stop(); + } catch (Exception e) { + Assert.fail("Should not throw Exception when service is stopped in initial state."); + } + } + + @Test + public void testGatewayManagerCaching() { + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverValid); + Optional gm1 = testService.findGateway(localAddressValid1); + Optional gm2 = testService.findGateway(localAddressValid1); + Assert.assertTrue("Expected gateway to be present.", gm1.isPresent()); + Assert.assertTrue("Expected gateway to be present.", gm2.isPresent()); + Assert.assertSame("Expected gateway managers to be cached.", gm1.get(), gm2.get()); + } + + @Test + public void testStopClearsPortMappings() throws IOException, SAXException { + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverValidMultiple); + Optional gm1 = testService.findGateway(localAddressValid1); + Optional gm2 = testService.findGateway(localAddressValid2); + Assert.assertTrue("Expected a valid gateway manager.", gm1.isPresent()); + Assert.assertTrue("Expected a valid gateway manager.", gm2.isPresent()); + + // add 4 port mappings to each gateway + int numPortsEach = 4; + gm1.get().addPortMapping(PORT_1, PORT_1, UpnpProtocol.UDP, PORT_MAPPING_DESCRIPTION_1); + gm1.get().addPortMapping(PORT_1, PORT_1, UpnpProtocol.TCP, PORT_MAPPING_DESCRIPTION_1); + gm1.get().addPortMapping(PORT_2, PORT_2, UpnpProtocol.UDP, PORT_MAPPING_DESCRIPTION_2); + gm1.get().addPortMapping(PORT_2, PORT_2, UpnpProtocol.TCP, PORT_MAPPING_DESCRIPTION_2); + + gm2.get().addPortMapping(PORT_1, PORT_1, UpnpProtocol.UDP, PORT_MAPPING_DESCRIPTION_1); + gm2.get().addPortMapping(PORT_1, PORT_1, UpnpProtocol.TCP, PORT_MAPPING_DESCRIPTION_1); + gm2.get().addPortMapping(PORT_2, PORT_2, UpnpProtocol.UDP, PORT_MAPPING_DESCRIPTION_2); + gm2.get().addPortMapping(PORT_2, PORT_2, UpnpProtocol.TCP, PORT_MAPPING_DESCRIPTION_2); + + // delete the port mappings and verify + testService.stop(); + + verify(mockGatewayValid1, timeout(STOP_UPNP_SERVICE_TIMEOUT).times(numPortsEach)) + .deletePortMapping(anyInt(), anyString()); + verify(mockGatewayValid2, timeout(STOP_UPNP_SERVICE_TIMEOUT).times(numPortsEach)) + .deletePortMapping(anyInt(), anyString()); + } + + @Test + public void testExceptionalQuery() { + UpnpService testService = createAndStartUpnpService(mockWildcardDiscoverExceptional); + Optional gm = testService.findGateway(wildcardAddress); + Assert.assertFalse( + "A query which throws exceptions should not return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testWildcardValid() { + UpnpService testService = createAndStartUpnpService(mockWildcardDiscoverValid); + Optional gm = testService.findGateway(wildcardAddress); + Assert.assertTrue( + "A query with valid results should return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testWildcardDefault() { + UpnpService testService = createAndStartUpnpService(mockWildcardDiscoverValid); + Optional gm = testService.findGateway(); + Assert.assertTrue( + "A query with valid results should return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testWildcardInvalid() { + UpnpService testService = createAndStartUpnpService(mockWildcardDiscoverInvalid); + Optional gm = testService.findGateway(wildcardAddress); + Assert.assertFalse( + "A query with no valid results should not return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testWildcardNone() { + UpnpService testService = createAndStartUpnpService(mockWildcardDiscoverNone); + Optional gm = testService.findGateway(wildcardAddress); + Assert.assertFalse( + "A query with no results should not return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testStringAddressValid() { + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverValid); + Optional gm = testService.findGateway(LOCAL_ADDRESS_VALID_1); + Assert.assertTrue( + "A valid local address String should return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testSpecificValid() throws IOException, SAXException { + when(mockGatewayValid1.addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString())) + .thenReturn(true); + + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverValid); + Optional gm = testService.findGateway(localAddressValid1); + Assert.assertTrue( + "A query with valid results should return a gateway manager.", + gm.isPresent() + ); + // assert that we used the network of the given local address by verifying the expected gateway + gm.get().addPortMapping(0, 0, UpnpProtocol.UDP, "unit test"); + + verify(mockGatewayValid1, description("Wrong local address connection was used.")) + .addPortMapping(anyInt(), anyInt(), anyString(), anyString(), anyString()); + } + + @Test + public void testSpecificInvalid() { + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverInvalid); + Optional gm = testService.findGateway(localAddressInvalid); + Assert.assertFalse( + "A query with no valid results should not return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testSpecificNone() { + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverNone); + Optional gm = testService.findGateway(localAddressValid1); + Assert.assertFalse( + "A query with no results should not return a gateway manager.", + gm.isPresent() + ); + } + + @Test + public void testSpecificThrowsExceptionOnValidation() throws IOException, SAXException { + doThrow(Exception.class).when(mockGatewayValid1).isConnected(); + + UpnpService testService = createAndStartUpnpService(mockSpecificDiscoverValid); + Optional gm = testService.findGateway(localAddressValid1); + Assert.assertFalse( + "An Exception thrown on status check should not result in a gateway manager being returned.", + gm.isPresent() + ); + } +} diff --git a/rskj-core/src/test/resources/test-rskj.conf b/rskj-core/src/test/resources/test-rskj.conf index 08085e01b22..e0db9e803ca 100644 --- a/rskj-core/src/test/resources/test-rskj.conf +++ b/rskj-core/src/test/resources/test-rskj.conf @@ -15,6 +15,10 @@ peer.discovery = { # peer [true/false] enabled = false + # Use UPnP to manage port forwarding on NAT router. + # Router must have UPnP enabled. + upnp_enabled = false + # number of workers that # tastes the peers for being # online [1..10]