Node can be configured to use an external relay for non-whitelisted inbound TCP connections.

It creates a SSH tunnel with the relay machine and forwards a remote port to the local message broker port.
This commit is contained in:
Andrius Dagys 2017-05-16 16:37:40 +01:00 committed by Mike Hearn
parent 782d4bd731
commit cdb222cff2
5 changed files with 63 additions and 1 deletions

View File

@ -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 :certificateSigningService: Certificate Signing Server address. It is used by the certificate signing request utility to
obtain SSL certificate. (See :doc:`permissioning` for more information.) 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``.

View File

@ -155,6 +155,9 @@ dependencies {
// FastClasspathScanner: classpath scanning // FastClasspathScanner: classpath scanning
compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20' 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 // Integration test helpers
integrationTestCompile "junit:junit:$junit_version" integrationTestCompile "junit:junit:$junit_version"
} }

View File

@ -3,6 +3,8 @@
package net.corda.node package net.corda.node
import com.jcabi.manifests.Manifests import com.jcabi.manifests.Manifests
import com.jcraft.jsch.JSch
import com.jcraft.jsch.JSchException
import com.typesafe.config.ConfigException import com.typesafe.config.ConfigException
import joptsimple.OptionException import joptsimple.OptionException
import net.corda.core.* 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.Node
import net.corda.node.internal.enforceSingleNodeIsRunning import net.corda.node.internal.enforceSingleNodeIsRunning
import net.corda.node.services.config.FullNodeConfiguration 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.services.transactions.bftSMaRtSerialFilter
import net.corda.node.shell.InteractiveShell import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
@ -21,6 +24,7 @@ import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole import org.fusesource.jansi.AnsiConsole
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.slf4j.bridge.SLF4JBridgeHandler import org.slf4j.bridge.SLF4JBridgeHandler
import java.io.IOException
import java.lang.management.ManagementFactory import java.lang.management.ManagementFactory
import java.net.InetAddress import java.net.InetAddress
import java.nio.file.Paths import java.nio.file.Paths
@ -104,7 +108,11 @@ fun main(args: Array<String>) {
println("Unable to load the configuration file: ${e.rootCause.message}") println("Unable to load the configuration file: ${e.rootCause.message}")
exitProcess(2) exitProcess(2)
} }
SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter)
conf.relay?.let { connectToRelay(it, conf.p2pAddress.port) }
if (cmdlineOptions.isRegistration) { if (cmdlineOptions.isRegistration) {
println() println()
println("******************************************************************") println("******************************************************************")
@ -171,6 +179,37 @@ fun main(args: Array<String>) {
exitProcess(0) 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 { private fun lookupMachineNameAndMaybeWarn(): String {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val hostName: String = InetAddress.getLocalHost().hostName val hostName: String = InetAddress.getLocalHost().hostName

View File

@ -58,6 +58,7 @@ data class FullNodeConfiguration(
@OldConfig("artemisAddress") @OldConfig("artemisAddress")
val p2pAddress: HostAndPort, val p2pAddress: HostAndPort,
val rpcAddress: 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. // 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 // 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?, 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)

View File

@ -193,7 +193,8 @@ fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int):
notaryNodeAddress = null, notaryNodeAddress = null,
notaryClusterAddresses = emptyList(), notaryClusterAddresses = emptyList(),
certificateChainCheckPolicies = emptyList(), certificateChainCheckPolicies = emptyList(),
devMode = true) devMode = true,
relay = null)
} }
@JvmOverloads @JvmOverloads