diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 39feba07ee..dcfa211d34 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -140,3 +140,14 @@ path to the node's base directory. :certificateSigningService: Certificate Signing Server address. It is used by the certificate signing request utility to obtain SSL certificate. (See :doc:`permissioning` for more information.) + +:relay: If provided, the node will attempt to tunnel inbound connections via an external relay. The relay's address will be + advertised to the network map service instead of the provided ``p2pAddress``. + + :relayHost: Hostname of the relay machine + :remoteInboundPort: A port on the relay machine that accepts incoming TCP connections. Traffic will be forwarded + from this port to the local port specified in ``p2pAddress``. + :username: Username for establishing a SSH connection with the relay. + :privateKeyFile: Path to the private key file for SSH authentication. The private key must not have a passphrase. + :publicKeyFile: Path to the public key file for SSH authentication. + :sshPort: Port to be used for SSH connection, default ``22``. diff --git a/node/build.gradle b/node/build.gradle index 4dc0c02b44..fc8632f7ae 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -155,6 +155,9 @@ dependencies { // FastClasspathScanner: classpath scanning compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20' + // Jsh: A SSH implementation for tunneling inbound traffic via a relay + compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' + // Integration test helpers integrationTestCompile "junit:junit:$junit_version" } diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index f293552a3c..7add085107 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -3,6 +3,8 @@ package net.corda.node import com.jcabi.manifests.Manifests +import com.jcraft.jsch.JSch +import com.jcraft.jsch.JSchException import com.typesafe.config.ConfigException import joptsimple.OptionException import net.corda.core.* @@ -13,6 +15,7 @@ import net.corda.core.utilities.Emoji import net.corda.node.internal.Node import net.corda.node.internal.enforceSingleNodeIsRunning import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.config.RelayConfiguration import net.corda.node.services.transactions.bftSMaRtSerialFilter import net.corda.node.shell.InteractiveShell import net.corda.node.utilities.registration.HTTPNetworkRegistrationService @@ -21,6 +24,7 @@ import org.fusesource.jansi.Ansi import org.fusesource.jansi.AnsiConsole import org.slf4j.LoggerFactory import org.slf4j.bridge.SLF4JBridgeHandler +import java.io.IOException import java.lang.management.ManagementFactory import java.net.InetAddress import java.nio.file.Paths @@ -104,7 +108,11 @@ fun main(args: Array) { println("Unable to load the configuration file: ${e.rootCause.message}") exitProcess(2) } + SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) + + conf.relay?.let { connectToRelay(it, conf.p2pAddress.port) } + if (cmdlineOptions.isRegistration) { println() println("******************************************************************") @@ -171,6 +179,37 @@ fun main(args: Array) { exitProcess(0) } +private fun connectToRelay(config: RelayConfiguration, localBrokerPort: Int) { + with(config) { + val jsh = JSch().apply { + val noPassphrase = byteArrayOf() + addIdentity(privateKeyFile.toString(), publicKeyFile.toString(), noPassphrase) + } + + val session = jsh.getSession(username, relayHost, sshPort).apply { + // We don't check the host fingerprints because they may change often + setConfig("StrictHostKeyChecking", "no") + } + + try { + log.info("Connecting to a relay at $relayHost") + session.connect() + } catch (e: JSchException) { + throw IOException("Unable to establish a SSH connection: $username@$relayHost", e) + } + try { + val localhost = "127.0.0.1" + log.info("Forwarding ports: $relayHost:$remoteInboundPort -> $localhost:$localBrokerPort") + session.setPortForwardingR(remoteInboundPort, localhost, localBrokerPort) + } catch (e: JSchException) { + throw IOException("Unable to set up port forwarding - is SSH on the remote host configured correctly? " + + "(port forwarding is not enabled by default)", e) + } + } + + log.info("Relay setup successfully!") +} + private fun lookupMachineNameAndMaybeWarn(): String { val start = System.currentTimeMillis() val hostName: String = InetAddress.getLocalHost().hostName diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 1ee3b213b0..51cd629c7e 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -58,6 +58,7 @@ data class FullNodeConfiguration( @OldConfig("artemisAddress") val p2pAddress: HostAndPort, val rpcAddress: HostAndPort?, + val relay: RelayConfiguration?, // TODO This field is slightly redundant as p2pAddress is sufficient to hold the address of the node's MQ broker. // Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one val messagingServerAddress: HostAndPort?, @@ -116,3 +117,10 @@ data class CertChainPolicyConfig(val role: String, val policy: CertChainPolicyTy } } } + +data class RelayConfiguration(val relayHost: String, + val remoteInboundPort: Int, + val username: String, + val privateKeyFile: Path, + val publicKeyFile: Path, + val sshPort: Int = 22) \ No newline at end of file diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index 35ae8093b2..a9911f39ed 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -193,7 +193,8 @@ fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int): notaryNodeAddress = null, notaryClusterAddresses = emptyList(), certificateChainCheckPolicies = emptyList(), - devMode = true) + devMode = true, + relay = null) } @JvmOverloads