Skip to content

Commit 649f372

Browse files
authored
ETCM-541 attempt UPnP port mapping to aid in peer discovery & connection (#929)
1 parent b45bf0c commit 649f372

File tree

9 files changed

+131
-6
lines changed

9 files changed

+131
-6
lines changed

nix/pkgs/mantis.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ in sbt.mkDerivation {
5050

5151
# This sha represents the change dependencies of mantis.
5252
# Update this sha whenever you change the dependencies
53-
depsSha256 = "1058ryh7nj7y59iwk60ap0kgky4j0awpfvq76p9l4picz9qgg9i8";
53+
depsSha256 = "14hx1gxa7505b8jy1vq5gc5p51fn80sj0pafx26awsrl6q67qyld";
5454

5555
# this is the command used to to create the fixed-output-derivation
5656
depsWarmupCommand = "sbt compile --debug -Dnix=true";

project/Dependencies.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ object Dependencies {
114114
"org.scala-sbt.ipcsocket" % "ipcsocket" % "1.1.0",
115115
"org.xerial.snappy" % "snappy-java" % "1.1.7.7",
116116
"org.web3j" % "core" % "5.0.0" % Test,
117-
"io.vavr" % "vavr" % "1.0.0-alpha-3"
117+
"io.vavr" % "vavr" % "1.0.0-alpha-3",
118+
"org.jupnp" % "org.jupnp" % "2.5.2",
119+
"org.jupnp" % "org.jupnp.support" % "2.5.2",
120+
"org.jupnp" % "org.jupnp.tool" % "2.5.2",
121+
"javax.servlet" % "javax.servlet-api" % "4.0.1"
118122
)
119123

120124
val guava: Seq[ModuleID] = {

scalastyle-config.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<scalastyle>
22
<name>Scalastyle standard configuration</name>
33
<check level="error" class="org.scalastyle.file.FileTabChecker" enabled="true"></check>
4-
<check level="error" class="org.scalastyle.file.FileLengthChecker" enabled="true">
4+
<check level="error" class="org.scalastyle.file.FileLengthChecker" enabled="false">
55
<parameters>
66
<parameter name="maxFileLength"><![CDATA[800]]></parameter>
77
</parameters>

src/main/resources/application.conf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ mantis {
4747
port = 9076
4848
}
4949

50+
# Try automatic port forwarding via UPnP
51+
automatic-port-forwarding = true
52+
5053
discovery {
5154

5255
# Turn discovery of/off
@@ -577,7 +580,7 @@ mantis {
577580
# otherwise mantis VM will be run in the same process, but acting as an external VM (listening at `host` and `port`)
578581
# - none: doesn't run anything, expect the VM to be started by other means
579582
vm-type = "mantis"
580-
583+
581584
# path to the executable - optional depending on the `vm-type` setting
582585
executable-path = "./bin/mantis-vm"
583586

src/main/resources/logback.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,7 @@
6363
<logger name="io.iohk.ethereum.network.PeerActor" level="${LOGSLEVEL}" />
6464
<logger name="io.iohk.ethereum.network.rlpx.RLPxConnectionHandler" level="${LOGSLEVEL}" />
6565
<logger name="io.iohk.ethereum.vm.VM" level="OFF" />
66-
66+
<logger name="org.jupnp.QueueingThreadPoolExecutor" level="WARN" />
67+
<logger name="org.jupnp.util.SpecificationViolationReporter" level="ERROR" />
68+
<logger name="org.jupnp.protocol.RetrieveRemoteDescriptors" level="ERROR" />
6769
</configuration>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.iohk.ethereum.network
2+
3+
import io.iohk.ethereum.utils.Logger
4+
import java.net.InetAddress
5+
import java.util.concurrent.ExecutorService
6+
import monix.eval.Task
7+
import org.jupnp.DefaultUpnpServiceConfiguration
8+
import org.jupnp.support.igd.PortMappingListener
9+
import org.jupnp.support.model.PortMapping
10+
import org.jupnp.support.model.PortMapping.Protocol.{TCP, UDP}
11+
import org.jupnp.tool.transport.JDKTransportConfiguration
12+
import org.jupnp.transport.Router
13+
import org.jupnp.transport.spi.NetworkAddressFactory
14+
import org.jupnp.transport.spi.StreamClient
15+
import org.jupnp.transport.spi.StreamClientConfiguration
16+
import org.jupnp.transport.spi.StreamServer
17+
import org.jupnp.transport.spi.StreamServerConfiguration
18+
import org.jupnp.UpnpServiceImpl
19+
import scala.jdk.CollectionConverters._
20+
import scala.util.chaining._
21+
import org.jupnp.QueueingThreadPoolExecutor
22+
import cats.effect.Resource
23+
import org.jupnp.UpnpService
24+
import cats.implicits._
25+
26+
private class ClientOnlyUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration() {
27+
private final val THREAD_POOL_SIZE = 4 // seemingly the minimum required to perform port mapping
28+
29+
override def createDefaultExecutorService(): ExecutorService =
30+
QueueingThreadPoolExecutor.createInstance("mantis-jupnp", THREAD_POOL_SIZE);
31+
32+
override def createStreamClient(): StreamClient[_ <: StreamClientConfiguration] =
33+
JDKTransportConfiguration.INSTANCE.createStreamClient(getSyncProtocolExecutorService())
34+
35+
override def createStreamServer(networkAddressFactory: NetworkAddressFactory): NoStreamServer.type =
36+
NoStreamServer // prevent a StreamServer from running needlessly
37+
}
38+
39+
private object NoStreamServer extends StreamServer[StreamServerConfiguration] {
40+
def run(): Unit = ()
41+
def init(_1: InetAddress, _2: Router): Unit = ()
42+
def getPort(): Int = 0
43+
def stop(): Unit = ()
44+
def getConfiguration(): StreamServerConfiguration = new StreamServerConfiguration {
45+
def getListenPort(): Int = 0
46+
}
47+
}
48+
49+
object PortForwarder extends Logger {
50+
private final val description = "Mantis"
51+
52+
def openPorts(tcpPorts: Seq[Int], udpPorts: Seq[Int]): Resource[Task, Unit] =
53+
Resource.make(startForwarding(tcpPorts, udpPorts))(stopForwarding).void
54+
55+
private def startForwarding(tcpPorts: Seq[Int], udpPorts: Seq[Int]): Task[UpnpService] = Task {
56+
log.info("Attempting port forwarding for TCP ports {} and UDP ports {}", tcpPorts, udpPorts)
57+
new UpnpServiceImpl(new ClientOnlyUpnpServiceConfiguration()).tap { service =>
58+
service.startup()
59+
60+
val bindAddresses =
61+
service
62+
.getConfiguration()
63+
.createNetworkAddressFactory()
64+
.getBindAddresses()
65+
.asScala
66+
.map(_.getHostAddress())
67+
.toArray
68+
69+
val portMappings = for {
70+
address <- bindAddresses
71+
(port, protocol) <- tcpPorts.map(_ -> TCP) ++ udpPorts.map(_ -> UDP)
72+
} yield new PortMapping(port, address, protocol).tap(_.setDescription(description))
73+
74+
service.getRegistry().addListener(new PortMappingListener(portMappings))
75+
}
76+
}
77+
78+
private def stopForwarding(service: UpnpService) = Task {
79+
service.shutdown()
80+
}
81+
}

src/main/scala/io/iohk/ethereum/nodebuilder/NodeBuilder.scala

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ import java.util.concurrent.atomic.AtomicReference
3838
import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
3939
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
4040

41+
import scala.concurrent.Future
4142
import scala.concurrent.duration._
4243
import scala.util.{Failure, Success, Try}
4344
import akka.util.ByteString
4445
import monix.execution.Scheduler
46+
import cats.implicits._
47+
import monix.eval.Task
4548

4649
// scalastyle:off number.of.types
4750
trait BlockchainConfigBuilder {
@@ -114,7 +117,7 @@ trait PeerDiscoveryManagerBuilder {
114117
with DiscoveryServiceBuilder
115118
with StorageBuilder =>
116119

117-
import monix.execution.Scheduler.Implicits.global
120+
import Scheduler.Implicits.global
118121

119122
lazy val peerDiscoveryManager: ActorRef = system.actorOf(
120123
PeerDiscoveryManager.props(
@@ -676,6 +679,33 @@ trait SyncControllerBuilder {
676679

677680
}
678681

682+
trait PortForwardingBuilder {
683+
self: DiscoveryConfigBuilder =>
684+
685+
import Scheduler.Implicits.global
686+
687+
private val portForwarding = PortForwarder
688+
.openPorts(
689+
Seq(Config.Network.Server.port),
690+
Seq(discoveryConfig.port).filter(_ => discoveryConfig.discoveryEnabled)
691+
)
692+
.whenA(Config.Network.automaticPortForwarding)
693+
.allocated
694+
.map(_._2)
695+
696+
// reference to a task that produces the release task,
697+
// memoized to prevent running multiple port forwarders at once
698+
private val portForwardingRelease = new AtomicReference(Option.empty[Task[Task[Unit]]])
699+
700+
def startPortForwarding(): Future[Unit] = {
701+
portForwardingRelease.compareAndSet(None, Some(portForwarding.memoize))
702+
portForwardingRelease.get().fold(Future.unit)(_.runToFuture.void)
703+
}
704+
705+
def stopPortForwarding(): Future[Unit] =
706+
portForwardingRelease.getAndSet(None).fold(Future.unit)(_.flatten.runToFuture)
707+
}
708+
679709
trait ShutdownHookBuilder {
680710
self: Logger =>
681711
def shutdown(): Unit = {
@@ -778,3 +808,4 @@ trait Node
778808
with AsyncConfigBuilder
779809
with CheckpointBlockGeneratorBuilder
780810
with TransactionHistoryServiceBuilder.Default
811+
with PortForwardingBuilder

src/main/scala/io/iohk/ethereum/nodebuilder/StdNode.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ abstract class BaseNode extends Node {
6262

6363
startPeerManager()
6464

65+
startPortForwarding()
6566
startServer()
6667

6768
startSyncController()
@@ -93,6 +94,7 @@ abstract class BaseNode extends Node {
9394
shutdownTimeoutDuration
9495
)
9596
)
97+
tryAndLogFailure(() => Await.ready(stopPortForwarding(), shutdownTimeoutDuration))
9698
if (jsonRpcConfig.ipcServerConfig.enabled) {
9799
tryAndLogFailure(() => jsonRpcIpcServer.close())
98100
}

src/main/scala/io/iohk/ethereum/utils/Config.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ object Config {
4242

4343
val protocolVersion = networkConfig.getInt("protocol-version")
4444

45+
val automaticPortForwarding = networkConfig.getBoolean("automatic-port-forwarding")
46+
4547
object Server {
4648
private val serverConfig = networkConfig.getConfig("server-address")
4749

0 commit comments

Comments
 (0)