From 6deb890396217fb858aebf34881574c10ed61dd9 Mon Sep 17 00:00:00 2001 From: rorp Date: Tue, 16 Oct 2018 22:43:13 -0700 Subject: [PATCH 01/40] [WIP] Tor hidden service --- eclair-core/pom.xml | 5 + eclair-core/src/main/resources/reference.conf | 9 + .../scala/fr/acinq/eclair/NodeParams.scala | 16 +- .../main/scala/fr/acinq/eclair/Setup.scala | 36 +- .../acinq/eclair/router/Announcements.scala | 3 +- .../scala/fr/acinq/eclair/router/Router.scala | 4 +- .../fr/acinq/eclair/tor/Controller.scala | 54 +++ .../fr/acinq/eclair/tor/OnionAddress.scala | 16 + .../acinq/eclair/tor/TorProtocolHandler.scala | 317 ++++++++++++++ .../scala/fr/acinq/eclair/tor/Version.scala | 26 ++ .../eclair/wire/LightningMessageCodecs.scala | 8 +- .../eclair/wire/LightningMessageTypes.scala | 39 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 8 +- .../eclair/router/AnnouncementsSpec.scala | 2 +- .../eclair/tor/TorProtocolHandlerSpec.scala | 402 ++++++++++++++++++ .../wire/LightningMessageCodecsSpec.scala | 14 + .../gui/controllers/MainController.scala | 2 +- 17 files changed, 931 insertions(+), 30 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala diff --git a/eclair-core/pom.xml b/eclair-core/pom.xml index a563e880b4..3405a7eb9e 100644 --- a/eclair-core/pom.xml +++ b/eclair-core/pom.xml @@ -165,6 +165,11 @@ scodec-core_${scala.version.short} 1.10.3 + + commons-codec + commons-codec + 1.9 + org.clapper diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1930bf281d..587a261845 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -79,4 +79,13 @@ eclair { max-pending-payment-requests = 10000000 max-payment-fee = 0.03 // max total fee for outgoing payments, in percentage: sending a payment will not be attempted if the cheapest route found is more expensive than that min-funding-satoshis = 100000 + + tor { + enabled = true + version = "v3" // v2 or v3 + host = "127.0.0.1" + proxy-port = 9050 + control-port = 9051 + private-key-file = "tor_pk" + } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index d49a87f8ef..b8c5ad1c58 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -24,13 +24,14 @@ import java.util.concurrent.TimeUnit import com.google.common.net.InetAddresses import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.{BinaryData, Block} +import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ -import fr.acinq.eclair.wire.Color +import fr.acinq.eclair.tor.OnionAddress +import fr.acinq.eclair.wire.{Color, NodeAddress} import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -75,9 +76,14 @@ case class NodeParams(keyManager: KeyManager, paymentRequestExpiry: FiniteDuration, maxPendingPaymentRequests: Int, maxPaymentFee: Double, - minFundingSatoshis: Long) { - val privateKey = keyManager.nodeKey.privateKey - val nodeId = keyManager.nodeId + minFundingSatoshis: Long, + torAddress: Option[OnionAddress] = None) { + val privateKey: Crypto.PrivateKey = keyManager.nodeKey.privateKey + val nodeId: Crypto.PublicKey = keyManager.nodeId + def nodeAddresses: List[NodeAddress] = torAddress + .map(NodeAddress(_)) + .map(List(_)) + .getOrElse(publicAddresses.map(NodeAddress(_))) } object NodeParams { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index f5a8be9736..5febfd82f4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -39,6 +39,7 @@ import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ +import fr.acinq.eclair.tor.{Controller, OnionAddress, TorProtocolHandler} import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray @@ -67,15 +68,19 @@ class Setup(datadir: File, val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) val chain = config.getString("chain") val keyManager = new LocalKeyManager(seed, NodeParams.makeChainHash(chain)) - val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager) + val initialNodeParams = NodeParams.makeNodeParams(datadir, config, keyManager) // early checks - DBCompatChecker.checkDBCompatibility(nodeParams) - DBCompatChecker.checkNetworkDBCompatibility(nodeParams) + DBCompatChecker.checkDBCompatibility(initialNodeParams) + DBCompatChecker.checkNetworkDBCompatibility(initialNodeParams) PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port")) + if (config.getBoolean("tor.enabled")) { + sys.props.put("socksProxyHost", config.getString("tor.host")) + sys.props.put("socksProxyPort", config.getInt("tor.proxy-port").toString) + } - logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") - logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") + logger.info(s"nodeid=${initialNodeParams.nodeId} alias=${initialNodeParams.alias}") + logger.info(s"using chain=$chain chainHash=${initialNodeParams.chainHash}") logger.info(s"initializing secure random generator") // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) @@ -86,6 +91,27 @@ class Setup(datadir: File, implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global + val nodeParams = if (config.getBoolean("tor.enabled")) { + val promiseTorAddress = Promise[OnionAddress]() + val protocolHandler = system.actorOf(TorProtocolHandler.props( + version = config.getString("tor.version"), + privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, + virtualPort = config.getInt("server.port"), + onionAdded = Some(promiseTorAddress), + nonce = None), + "tor-proto") + + val controller = system.actorOf(Controller.props( + address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.control-port")), + protocolHandler = protocolHandler), "tor") + + val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") + logger.info(s"Tor address ${torAddress.toOnion}") + initialNodeParams.copy(torAddress = Some(torAddress)) + } else { + initialNodeParams + } + val bitcoin = nodeParams.watcherType match { case BITCOIND => val bitcoinClient = new BasicBitcoinJsonRPCClient( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index a4d329f49a..898284fc1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -75,9 +75,8 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { require(alias.size <= 32) - val nodeAddresses = addresses.map(NodeAddress(_)) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", nodeAddresses) val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte NodeAnnouncement( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 19c73b8b23..ea16dc692e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -140,7 +140,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.nodeAddresses) self ! nodeAnn log.info(s"initialization completed, ready to process messages") @@ -232,7 +232,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // in case we just validated our first local channel, we announce the local node if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.nodeAddresses) self ! nodeAnn } true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala new file mode 100644 index 0000000000..7e744af8d4 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala @@ -0,0 +1,54 @@ +package fr.acinq.eclair.tor + +import java.net.InetSocketAddress + +import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import akka.io.{IO, Tcp} +import akka.util.ByteString + +import scala.concurrent.ExecutionContext + +class Controller(address: InetSocketAddress, listener: ActorRef) + (implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { + + import Controller._ + import Tcp._ + import context.system + + IO(Tcp) ! Connect(address) + + def receive = { + case e@CommandFailed(_: Connect) => + e.cause match { + case Some(ex) => log.error(ex, "Cannot connect") + case _ => log.error("Cannot connect") + } + context stop listener + context stop self + case c@Connected(remote, local) => + listener ! c + val connection = sender() + connection ! Register(self) + context become { + case data: ByteString => + connection ! Write(data) + case CommandFailed(w: Write) => + // O/S buffer was full + listener ! SendFailed + log.error("Tor command failed") + case Received(data) => + listener ! data + case _: ConnectionClosed => + context stop listener + context stop self + } + } + +} + +object Controller { + def props(address: InetSocketAddress, protocolHandler: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.global) = + Props(new Controller(address, protocolHandler)) + + case object SendFailed +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala new file mode 100644 index 0000000000..0faff6846c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala @@ -0,0 +1,16 @@ +package fr.acinq.eclair.tor + +import org.apache.commons.codec.binary.Base32 + +sealed trait OnionAddress { + val onionService: String + + val port: Int + + def toOnion: String = s"$onionService.onion:$port" + + def decodedOnionService: Array[Byte] = new Base32().decode(onionService.toUpperCase) +} +case class OnionAddressV2(onionService: String, port: Int) extends OnionAddress { require(onionService.length == 16) } +case class OnionAddressV3(onionService: String, port: Int) extends OnionAddress { require(onionService.length == 56) } + diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala new file mode 100644 index 0000000000..816ec48497 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -0,0 +1,317 @@ +package fr.acinq.eclair.tor + +import java.io._ +import java.nio.file.{Files, Paths} +import java.security.SecureRandom + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash, Status} +import akka.io.Tcp.Connected +import akka.util.ByteString +import fr.acinq.eclair.tor.TorProtocolHandler.ProtocolVersion +import fr.acinq.eclair.randomBytes +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import javax.xml.bind.DatatypeConverter + +import scala.concurrent.Promise +import scala.util.Random + +case class TorException(msg: String) extends RuntimeException(msg) + +class TorProtocolHandler(protocolVersion: ProtocolVersion, + privateKeyPath: String, + virtualPort: Int, + targetPorts: Seq[Int], + onionAdded: Option[Promise[OnionAddress]], + clientNonce: Option[Array[Byte]] + ) extends Actor with Stash with ActorLogging { + + import TorProtocolHandler._ + + private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() + private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() + + private var receiver: ActorRef = _ + + private var protoInfo: ProtocolInfo = _ + private var clientHash: Array[Byte] = _ + private var keyStr: String = _ + private var portStr: String = _ + + private var address: Option[OnionAddress] = None + + private val nonce: Array[Byte] = clientNonce.getOrElse(randomBytes(32)) + + override def receive: Receive = { + case Connected(_, _) => + receiver = sender() + context become protocolInfo + self ! SendNextCommand + } + + def protocolInfo: Receive = { + case SendNextCommand => + sendCommand("PROTOCOLINFO 1") + case data: ByteString => handleExceptions { + val res = parseResponse(readResponse(data)) + protoInfo = ProtocolInfo( + methods = res.getOrElse("METHODS", throw TorException("Tor auth methods not found")), + cookieFile = unquote(res.getOrElse("COOKIEFILE", throw TorException("Tor cookie file not found"))), + version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found")))) + log.info(s"Tor version ${protoInfo.version}") + if (!protocolVersion.supportedBy(protoInfo.version)) { + throw TorException(s"Tor version ${protoInfo.version} does not support protocol $protocolVersion") + } + context become authChallenge + self ! SendNextCommand + } + } + + def authChallenge: Receive = { + case SendNextCommand => + sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") + case data: ByteString => handleExceptions { + val res = parseResponse(readResponse(data)) + clientHash = computeClientHash( + res.getOrElse("SERVERHASH", throw TorException("Tor server hash not found")), + res.getOrElse("SERVERNONCE", throw TorException("Tor server nonce not found")) + ) + context become authenticate + self ! SendNextCommand + } + } + + def authenticate: Receive = { + case SendNextCommand => + sendCommand(s"AUTHENTICATE ${hex(clientHash)}") + case data: ByteString => handleExceptions { + readResponse(data) + keyStr = computeKey + portStr = computePort + context become addOnion + self ! SendNextCommand + } + } + + def addOnion: Receive = { + case SendNextCommand => + val cmd = s"ADD_ONION $keyStr $portStr" + sendCommand(cmd) + case data: ByteString => handleExceptions { + val res = readResponse(data) + if (ok(res)) { + val serviceId = processOnionResponse(parseResponse(res)) + address = Some(protocolVersion match { + case V2 => OnionAddressV2(serviceId, virtualPort) + case V3 => OnionAddressV3(serviceId, virtualPort) + }) + onionAdded.foreach(_.success(address.get)) + log.debug(s"Onion address: ${address.get}") + } + } + } + + override def unhandled(message: Any): Unit = message match { + case GetOnionAddress => + sender() ! address + } + + private def handleExceptions[T](f: => T): Unit = try { + f + } catch { + case e: Exception => + log.error(e, "Tor error: ") + sender ! Status.Failure(e) + } + + private def processOnionResponse(res: Map[String, String]): String = { + val serviceId = res.getOrElse("ServiceID", throw TorException("Tor service ID not found")) + val privateKey = res.get("PrivateKey") + privateKey.foreach(writeString(privateKeyPath, _)) + serviceId + } + + private def computeKey: String = { + if (Files.exists(Paths.get(privateKeyPath))) { + readString(privateKeyPath) + } else { + protocolVersion match { + case V2 => "NEW:RSA1024" + case V3 => "NEW:ED25519-V3" + } + } + } + + private def computePort: String = { + if (targetPorts.isEmpty) { + s"Port=$virtualPort,$virtualPort" + } else { + targetPorts.map(p => s"Port=$virtualPort,$p").mkString(" ") + } + } + + private def computeClientHash(serverHash: String, serverNonce: String): Array[Byte] = { + val decodedServerHash = unhex(serverHash) + if (decodedServerHash.length != 32) + throw TorException("Invalid server hash length") + + val decodedServerNonce = unhex(serverNonce) + if (decodedServerNonce.length != 32) + throw TorException("Invalid server nonce length") + + val cookie = readBytes(protoInfo.cookieFile, 32) + + val message = cookie ++ nonce ++ decodedServerNonce + + val computedServerHash = hex(hmacSHA256(ServerKey, message)) + if (computedServerHash != serverHash) { + throw TorException("Unexpected server hash") + } + + hmacSHA256(ClientKey, message) + } + + private def sendCommand(cmd: String): Unit = { + receiver ! ByteString(s"$cmd\r\n") + } +} + +object TorProtocolHandler { + def props(version: String, + privateKeyPath: String, + virtualPort: Int, + targetPorts: Seq[Int] = Seq(), + onionAdded: Option[Promise[OnionAddress]] = None, + nonce: Option[Array[Byte]] = None + ): Props = + Props(new TorProtocolHandler(ProtocolVersion(version), privateKeyPath, virtualPort, targetPorts, onionAdded, nonce)) + + val MinV3Version = "0.3.3.6" + + sealed trait ProtocolVersion { + def supportedBy(torVersion: String): Boolean + } + + case object V2 extends ProtocolVersion { + override def supportedBy(torVersion: String): Boolean = true + } + + case object V3 extends ProtocolVersion { + override def supportedBy(torVersion: String): Boolean = Version(torVersion) >= Version(MinV3Version) + } + + object ProtocolVersion { + def apply(s: String): ProtocolVersion = s match { + case "v2" | "V2" => V2 + case "v3" | "V3" => V3 + case _ => throw TorException(s"Unknown protocol version `$s`") + } + } + + case object SendNextCommand + + case object GetOnionAddress + + case object AuthCompleted + + case object AuthFailed + + case class OnionAdded(onionAddress: OnionAddress) + + case class Error(ex: Throwable) + + case class ProtocolInfo(methods: String, cookieFile: String, version: String) + + def readBytes(filename: String, n: Int): Array[Byte] = { + val bytes = Array.ofDim[Byte](1024) + val s = new FileInputStream(filename) + try { + if (s.read(bytes) != n) + throw TorException("Invalid file length") + bytes.take(n) + } finally { + s.close() + } + } + + def writeBytes(filename: String, bytes: Array[Byte]): Unit = { + val s = new FileOutputStream(filename) + try { + s.write(bytes) + } finally { + s.close() + } + } + + + def readString(filename: String): String = { + val r = new BufferedReader(new FileReader(filename)) + try { + r.readLine() + } finally { + r.close() + } + } + + def writeString(filename: String, string: String): Unit = { + val w = new PrintWriter(new OutputStreamWriter(new FileOutputStream(filename))) + try { + w.print(string) + } finally { + w.close() + } + } + + def unquote(s: String): String = s + .stripSuffix("\"") + .stripPrefix("\"") + .replace("""\\""", """\""") + .replace("""\"""", "\"") + + def hex(buf: Seq[Byte]): String = buf.map("%02X" format _).mkString + + def unhex(hexString: String): Array[Byte] = DatatypeConverter.parseHexBinary(hexString) + + private val r1 = """(\d+)\-(.*)""".r + private val r2 = """(\d+) (.*)""".r + + def readResponse(bstr: ByteString): Seq[(Int, String)] = { + val lines = bstr.utf8String.split('\n') + .map(_.stripSuffix("\r")) + .filterNot(_.isEmpty) + .map { + case r1(c, msg) => (c.toInt, msg) + case r2(c, msg) => (c.toInt, msg) + case x@_ => throw TorException(s"Unknown response line format: `$x`") + } + if (!ok(lines)) { + throw TorException(s"Tor server returned error: ${status(lines)} ${reason(lines)}") + } + lines + } + + def ok(res: Seq[(Int, String)]): Boolean = status(res) == 250 + + def status(res: Seq[(Int, String)]): Int = res.lastOption.map(_._1).getOrElse(-1) + + def reason(res: Seq[(Int, String)]): String = res.lastOption.map(_._2).getOrElse("Unknown error") + + private val r = """([^=]+)=(.+)""".r + + def parseResponse(lines: Seq[(Int, String)]): Map[String, String] = { + lines.flatMap { + case (_, message) => + message.split(" ") + .collect { + case r(k, v) => (k, v) + } + }.toMap + } + + def hmacSHA256(key: Array[Byte], message: Array[Byte]): Array[Byte] = { + val mac = Mac.getInstance("HmacSHA256") + val secretKey = new SecretKeySpec(key, "HmacSHA256") + mac.init(secretKey) + mac.doFinal(message) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala new file mode 100644 index 0000000000..7a6bede1eb --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala @@ -0,0 +1,26 @@ +package fr.acinq.eclair.tor + +import scala.util.Try + +case class Version(value: String) extends Ordered[Version] { + lazy val parts: Seq[String] = { + val ps = value.split('.') + // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) + ps.update(ps.length - 1, ps.last.split('-').head) + ps + } + + override def compare(that: Version): Int = { + parts + .zipAll(that.parts, "", "") + .find { case (a, b) => a != b } + .map { case (a, b) => + (for { + x <- Try(a.toInt) + y <- Try(b.toInt) + } yield x.compare(y)) + .getOrElse(a.compare(b)) + } + .getOrElse(0) + } +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 221a194c04..9e5de8015c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -61,10 +61,10 @@ object LightningMessageCodecs { def nodeaddress: Codec[NodeAddress] = discriminated[NodeAddress].by(uint8) .typecase(0, provide(Padding)) - .typecase(1, (ipv4address ~ uint16).xmap[IPv4](x => new IPv4(x._1, x._2), x => (x.ipv4, x.port))) - .typecase(2, (ipv6address ~ uint16).xmap[IPv6](x => new IPv6(x._1, x._2), x => (x.ipv6, x.port))) - .typecase(3, (binarydata(10) ~ uint16).xmap[Tor2](x => new Tor2(x._1, x._2), x => (x.tor2, x.port))) - .typecase(4, (binarydata(35) ~ uint16).xmap[Tor3](x => new Tor3(x._1, x._2), x => (x.tor3, x.port))) + .typecase(1, (ipv4address ~ uint16).xmap[IPv4](x => IPv4(x._1, x._2), x => (x.ipv4, x.port))) + .typecase(2, (ipv6address ~ uint16).xmap[IPv6](x => IPv6(x._1, x._2), x => (x.ipv6, x.port))) + .typecase(3, (binarydata(10) ~ uint16).xmap[Tor2](x => Tor2(x._1, x._2), x => (x.tor2, x.port))) + .typecase(4, (binarydata(35) ~ uint16).xmap[Tor3](x => Tor3(x._1, x._2), x => (x.tor3, x.port))) // this one is a bit different from most other codecs: the first 'len' element is * not * the number of items // in the list but rather the number of bytes of the encoded list. The rationale is once we've read this diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 227bcc4a82..2e3d368d1c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -20,6 +20,7 @@ import java.net.{Inet4Address, Inet6Address, InetSocketAddress} import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} +import fr.acinq.eclair.tor.{OnionAddress, OnionAddressV2, OnionAddressV3} import fr.acinq.eclair.{ShortChannelId, UInt64} /** @@ -160,19 +161,45 @@ case class Color(r: Byte, g: Byte, b: Byte) { } // @formatter:off -sealed trait NodeAddress +sealed trait NodeAddress { + def getHostString: String + def getPort: Int +} case object NodeAddress { def apply(inetSocketAddress: InetSocketAddress): NodeAddress = inetSocketAddress.getAddress match { case a: Inet4Address => IPv4(a, inetSocketAddress.getPort) case a: Inet6Address => IPv6(a, inetSocketAddress.getPort) case _ => throw new RuntimeException(s"Invalid socket address $inetSocketAddress") } + def apply(onionAddress: OnionAddress): NodeAddress = { + onionAddress match { + case _: OnionAddressV2 => Tor2(BinaryData(onionAddress.decodedOnionService), onionAddress.port) + case _: OnionAddressV3 => Tor3(BinaryData(onionAddress.decodedOnionService), onionAddress.port) + } + } +} +case object Padding extends NodeAddress { + override def getHostString: String = "" + override def getPort: Int = 0 +} +case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { + override def getHostString: String = ipv4.getHostAddress + override def getPort: Int = port +} +case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { + override def getHostString: String = ipv6.getHostAddress + override def getPort: Int = port +} +case class Tor2(tor2: BinaryData, port: Int) extends NodeAddress { + require(tor2.size == 10) + override def getHostString: String = tor2.toString() + override def getPort: Int = port +} +case class Tor3(tor3: BinaryData, port: Int) extends NodeAddress { + require(tor3.size == 35) + override def getHostString: String = tor3.toString() + override def getPort: Int = port } -case object Padding extends NodeAddress -case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress -case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress -case class Tor2(tor2: BinaryData, port: Int) extends NodeAddress { require(tor2.size == 10) } -case class Tor3(tor3: BinaryData, port: Int) extends NodeAddress { require(tor3.size == 35) } // @formatter:on case class NodeAnnouncement(signature: BinaryData, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index b447321828..a48628ea65 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -22,7 +22,7 @@ import java.sql.DriverManager import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.wire.Color +import fr.acinq.eclair.wire.{Color, NodeAddress} import fr.acinq.eclair.{ShortChannelId, randomKey} import org.scalatest.FunSuite import org.sqlite.SQLiteException @@ -42,9 +42,9 @@ class SqliteNetworkDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) assert(db.listNodes().toSet === Set.empty) db.addNode(node_1) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 998b69d589..fda7cb193c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -47,7 +47,7 @@ class AnnouncementsSpec extends FunSuite { } test("create valid signed node announcement") { - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.nodeAddresses) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala new file mode 100644 index 0000000000..da3956e5c7 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -0,0 +1,402 @@ +package fr.acinq.eclair.tor + +import java.io.{File, IOException} +import java.net.InetSocketAddress +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} + +import akka.actor.ActorSystem +import akka.actor.Status.Failure +import akka.io.Tcp.Connected +import akka.testkit.{ImplicitSender, TestActorRef, TestKit} +import akka.util.ByteString +import fr.acinq.eclair.wire.NodeAddress +import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpecLike} + +import scala.concurrent.duration._ +import scala.concurrent.{Await, Promise} + +class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) + with ImplicitSender + with WordSpecLike + with MustMatchers + with BeforeAndAfterAll { + + import TorProtocolHandler._ + + override protected def afterAll(): Unit = { + super.afterAll() + system.terminate() + } + + val LocalHost = new InetSocketAddress("localhost", 8888) + val ClientNonce = "8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA" + val AuthCookie = "AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485" + + "tor" ignore { + sys.props.put("socksProxyHost", "localhost") + sys.props.put("socksProxyPort", "9050") + +// val u = new URL("http://bitcoin.org/") +// val c = u.openConnection() +// val r = new BufferedReader(new InputStreamReader(c.getInputStream)) +// try { +// r.lines().forEach(new Consumer[String] { +// override def accept(t: String): Unit = { +// println(t) +// } +// }) +// } finally { +// r.close() +// } + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(TorProtocolHandler.props( + version ="v2", + privateKeyPath = sys.props("user.home") + "/v2_pk", + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = None), //Some(unhex(ClientNonce))), + "tor-proto") + + val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandler), "tor") + + val address = Await.result(promiseOnionAddress.future, 30 seconds) + println(address) + println(address.onionService.length) + println((address.onionService + ".onion").length) + println(NodeAddress(address)) + } + + "happy path v2" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v2", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "happy-v2") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) + + expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "250-ServiceID=z4zif3fy7fe7bpg3\r\n" + + "250-PrivateKey=RSA1024:private-key\r\n" + + "250 OK\r\n" + ) + + protocolHandler ! GetOnionAddress + expectMsg(Some(OnionAddressV2("z4zif3fy7fe7bpg3", 9999))) + + val address = Await.result(promiseOnionAddress.future, 3 seconds) + address must be(OnionAddressV2("z4zif3fy7fe7bpg3", 9999)) + address.toOnion must be ("z4zif3fy7fe7bpg3.onion:9999") + NodeAddress(address).toString must be ("Tor2(cf3282ecb8f949f0bcdb,9999)") + + readString(pkFile) must be("RSA1024:private-key") + } + } + + "happy path v3" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v3", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "happy-v3") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.4.8\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) + + expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "250-ServiceID=mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd\r\n" + + "250-PrivateKey=ED25519-V3:private-key\r\n" + + "250 OK\r\n" + ) + + protocolHandler ! GetOnionAddress + expectMsg(Some(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) + + val address = Await.result(promiseOnionAddress.future, 3 seconds) + address must be(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) + address.toOnion must be ("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") + NodeAddress(address).toString must be ("Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") + + readString(pkFile) must be("ED25519-V3:private-key") + } + } + + "v3 should not be supported by 0.3.3.5" in { + val protocolHandler = TestActorRef(props( + version = "v3", + privateKeyPath = "", + virtualPort = 9999, + onionAdded = None, + nonce = Some(unhex(ClientNonce))), "unsupported-v3") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"cookieFile\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(Failure(TorException("Tor version 0.3.3.5 does not support protocol V3"))) + } + + "v3 should handle AUTHCHALLENGE errors" in { + + val protocolHandler = TestActorRef(props( + version = "v3", + privateKeyPath = "", + virtualPort = 9999, + onionAdded = None, + nonce = Some(unhex(ClientNonce))), "authchallenge-error") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"cookieFile\"\r\n" + + "250-VERSION Tor=\"0.3.4.8\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "513 Invalid base16 client nonce\r\n" + ) + + expectMsg(Failure(TorException("Tor server returned error: 513 Invalid base16 client nonce"))) + } + + "v2 should handle invalid cookie file" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie).take(2)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v2", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "invalid-cookie") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(Failure(TorException("Invalid file length"))) + } + } + + "v2 should handle server hash error" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v2", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "server-hash-error") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50D0 SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(Failure(TorException("Unexpected server hash"))) + } + } + + "v2 should handle AUTHENTICATE failure" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v2", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "auth-error") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) + protocolHandler ! ByteString( + "515 Authentication failed: Safe cookie response did not match expected value.\r\n" + ) + expectMsg(Failure(TorException("Tor server returned error: 515 Authentication failed: Safe cookie response did not match expected value."))) + } + } + + "v2 should handle ADD_ONION failure" in { + withTempDir { dir => + val cookieFile = dir + File.separator + "cookie" + val pkFile = dir + File.separator + "pk" + + writeBytes(cookieFile, unhex(AuthCookie)) + + val promiseOnionAddress = Promise[OnionAddress]() + + val protocolHandler = TestActorRef(props( + version = "v2", + privateKeyPath = pkFile, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress), + nonce = Some(unhex(ClientNonce))), "add-onion-error") + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) + + expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "513 Invalid argument\r\n" + ) + + expectMsg(Failure(TorException("Tor server returned error: 513 Invalid argument"))) + } + } + + def withTempDir[T](f: String => T): T = { + val d = Files.createTempDirectory("test-") + try { + f(d.toString) + } finally { + Files.walkFileTree(d, new SimpleFileVisitor[Path]() { + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + Files.delete(file) + FileVisitResult.CONTINUE + } + + override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { + Files.delete(dir) + FileVisitResult.CONTINUE + } + }) + } + } +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index fb32f78d99..7111d0f783 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -100,6 +100,20 @@ class LightningMessageCodecsSpec extends FunSuite { val nodeaddr2 = nodeaddress.decode(bin).require.value assert(nodeaddr === nodeaddr2) } + { + val nodeaddr = Tor2(Array.tabulate(10)(_.toByte), 4231) + val bin = nodeaddress.encode(nodeaddr).require + assert(bin === hex"03 00 01 02 03 04 05 06 07 08 09 10 87".toBitVector) + val nodeaddr2 = nodeaddress.decode(bin).require.value + assert(nodeaddr === nodeaddr2) + } + { + val nodeaddr = Tor3(Array.tabulate(35)(_.toByte), 4231) + val bin = nodeaddress.encode(nodeaddr).require + assert(bin === hex"04 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 10 87".toBitVector) + val nodeaddr2 = nodeaddress.decode(bin).require.value + assert(nodeaddr === nodeaddr2) + } } test("encode/decode with signature codec") { diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index 628d263287..86e67f2019 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -365,7 +365,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext bitcoinChain.setText(s"${setup.chain.toUpperCase()}") bitcoinChain.getStyleClass.add(setup.chain) - val nodeURI_opt = setup.nodeParams.publicAddresses.headOption.map(address => { + val nodeURI_opt = setup.nodeParams.nodeAddresses.headOption.map(address => { s"${setup.nodeParams.nodeId}@${HostAndPort.fromParts(address.getHostString, address.getPort)}" }) From 1f299eef2b7dab0139d44c3981611c1a4864131a Mon Sep 17 00:00:00 2001 From: rorp Date: Thu, 18 Oct 2018 22:53:59 -0700 Subject: [PATCH 02/40] wire support for Tor addresses --- eclair-core/src/main/resources/reference.conf | 6 +- .../scala/fr/acinq/eclair/NodeParams.scala | 14 +--- .../main/scala/fr/acinq/eclair/Setup.scala | 81 ++++++++++--------- .../fr/acinq/eclair/api/JsonSerializers.scala | 5 +- .../eclair/db/sqlite/SqlitePeersDb.scala | 7 +- .../acinq/eclair/router/Announcements.scala | 5 +- .../scala/fr/acinq/eclair/router/Router.scala | 8 +- .../fr/acinq/eclair/tor/OnionAddress.scala | 59 ++++++++++++-- .../eclair/wire/LightningMessageTypes.scala | 26 +++--- .../eclair/api/JsonSerializersSpec.scala | 5 ++ .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 17 ++-- .../eclair/router/AnnouncementsSpec.scala | 2 +- .../gui/controllers/MainController.scala | 2 +- 13 files changed, 154 insertions(+), 83 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 587a261845..68e3afe9fe 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -81,10 +81,10 @@ eclair { min-funding-satoshis = 100000 tor { - enabled = true - version = "v3" // v2 or v3 + enabled = false + protocol = "v3" // socks, v2, v3 host = "127.0.0.1" - proxy-port = 9050 + socks-port = 9050 control-port = 9051 private-key-file = "tor_pk" } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index b8c5ad1c58..62d1f8dec5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -30,8 +30,7 @@ import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ -import fr.acinq.eclair.tor.OnionAddress -import fr.acinq.eclair.wire.{Color, NodeAddress} +import fr.acinq.eclair.wire.Color import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -76,14 +75,9 @@ case class NodeParams(keyManager: KeyManager, paymentRequestExpiry: FiniteDuration, maxPendingPaymentRequests: Int, maxPaymentFee: Double, - minFundingSatoshis: Long, - torAddress: Option[OnionAddress] = None) { + minFundingSatoshis: Long) { val privateKey: Crypto.PrivateKey = keyManager.nodeKey.privateKey val nodeId: Crypto.PublicKey = keyManager.nodeId - def nodeAddresses: List[NodeAddress] = torAddress - .map(NodeAddress(_)) - .map(List(_)) - .getOrElse(publicAddresses.map(NodeAddress(_))) } object NodeParams { @@ -128,7 +122,7 @@ object NodeParams { } } - def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager): NodeParams = { + def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager, publicAddress: Option[InetSocketAddress]): NodeParams = { datadir.mkdirs() @@ -170,7 +164,7 @@ object NodeParams { keyManager = keyManager, alias = config.getString("node-alias").take(32), color = Color(color.data(0), color.data(1), color.data(2)), - publicAddresses = config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))), + publicAddresses = publicAddress.map(List(_)).getOrElse(config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port")))), globalFeatures = BinaryData(config.getString("global-features")), localFeatures = BinaryData(config.getString("local-features")), dustLimitSatoshis = dustLimitSatoshis, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 5febfd82f4..0e4167e228 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -43,8 +43,8 @@ import fr.acinq.eclair.tor.{Controller, OnionAddress, TorProtocolHandler} import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray -import scala.concurrent.duration._ import scala.concurrent._ +import scala.concurrent.duration._ /** * Setup eclair from a datadir. @@ -68,49 +68,26 @@ class Setup(datadir: File, val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) val chain = config.getString("chain") val keyManager = new LocalKeyManager(seed, NodeParams.makeChainHash(chain)) - val initialNodeParams = NodeParams.makeNodeParams(datadir, config, keyManager) - - // early checks - DBCompatChecker.checkDBCompatibility(initialNodeParams) - DBCompatChecker.checkNetworkDBCompatibility(initialNodeParams) - PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port")) - if (config.getBoolean("tor.enabled")) { - sys.props.put("socksProxyHost", config.getString("tor.host")) - sys.props.put("socksProxyPort", config.getInt("tor.proxy-port").toString) - } - - logger.info(s"nodeid=${initialNodeParams.nodeId} alias=${initialNodeParams.alias}") - logger.info(s"using chain=$chain chainHash=${initialNodeParams.chainHash}") + implicit val materializer = ActorMaterializer() + implicit val timeout = Timeout(30 seconds) + implicit val formats = org.json4s.DefaultFormats + implicit val ec = ExecutionContext.Implicits.global logger.info(s"initializing secure random generator") // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) secureRandom.nextInt() - implicit val materializer = ActorMaterializer() - implicit val timeout = Timeout(30 seconds) - implicit val formats = org.json4s.DefaultFormats - implicit val ec = ExecutionContext.Implicits.global + val torPublicAddress = initTor() - val nodeParams = if (config.getBoolean("tor.enabled")) { - val promiseTorAddress = Promise[OnionAddress]() - val protocolHandler = system.actorOf(TorProtocolHandler.props( - version = config.getString("tor.version"), - privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, - virtualPort = config.getInt("server.port"), - onionAdded = Some(promiseTorAddress), - nonce = None), - "tor-proto") - - val controller = system.actorOf(Controller.props( - address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.control-port")), - protocolHandler = protocolHandler), "tor") - - val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") - logger.info(s"Tor address ${torAddress.toOnion}") - initialNodeParams.copy(torAddress = Some(torAddress)) - } else { - initialNodeParams - } + val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, torPublicAddress) + + // early checks + DBCompatChecker.checkDBCompatibility(nodeParams) + DBCompatChecker.checkNetworkDBCompatibility(nodeParams) + PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port")) + + logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") + logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") val bitcoin = nodeParams.watcherType match { case BITCOIND => @@ -285,6 +262,34 @@ class Setup(datadir: File, throw e } + private def initTor(): Option[InetSocketAddress] = { + if (config.getBoolean("tor.enabled")) { + sys.props.put("socksProxyHost", config.getString("tor.host")) + sys.props.put("socksProxyPort", config.getInt("tor.socks-port").toString) + if (config.getString("tor.protocol") != "socks") { + val promiseTorAddress = Promise[OnionAddress]() + val protocolHandler = system.actorOf(TorProtocolHandler.props( + version = config.getString("tor.protocol"), + privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, + virtualPort = config.getInt("server.port"), + onionAdded = Some(promiseTorAddress), + nonce = None), + "tor-proto") + + val controller = system.actorOf(Controller.props( + address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.control-port")), + protocolHandler = protocolHandler), "tor") + + val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") + logger.info(s"Tor address ${torAddress.toOnion}") + Option(torAddress.toInetSocketAddress) + } else { + None + } + } else { + None + } + } } // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index 63d8a83036..719f544119 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -24,6 +24,7 @@ import fr.acinq.bitcoin.{BinaryData, MilliSatoshi, OutPoint, Transaction} import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.router.RouteResponse +import fr.acinq.eclair.tor.OnionAddress import fr.acinq.eclair.transactions.Direction import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ @@ -124,8 +125,8 @@ class FailureMessageSerializer extends CustomSerializer[FailureMessage](format = class NodeAddressSerializer extends CustomSerializer[NodeAddress](format => ({ null},{ case IPv4(a, p) => JString(HostAndPort.fromParts(a.getHostAddress, p).toString) case IPv6(a, p) => JString(HostAndPort.fromParts(a.getHostAddress, p).toString) - case Tor2(b, p) => JString(s"${b.toString}:$p") - case Tor3(b, p) => JString(s"${b.toString}:$p") + case Tor2(b, p) => JString(HostAndPort.fromParts(OnionAddress.hostString(b), p).toString) + case Tor3(b, p) => JString(HostAndPort.fromParts(OnionAddress.hostString(b), p).toString) })) class DirectionSerializer extends CustomSerializer[Direction](format => ({ null },{ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 4ce0ea97a5..9052808c63 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -16,14 +16,15 @@ package fr.acinq.eclair.db.sqlite -import java.net.{Inet4Address, Inet6Address, InetSocketAddress} +import java.net.InetSocketAddress import java.sql.Connection import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.db.PeersDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.wire.{IPv4, IPv6, LightningMessageCodecs, NodeAddress} +import fr.acinq.eclair.tor.OnionAddress +import fr.acinq.eclair.wire._ import scodec.bits.BitVector class SqlitePeersDb(sqlite: Connection) extends PeersDb { @@ -68,6 +69,8 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb { val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value match { case IPv4(ipv4, port) => new InetSocketAddress(ipv4, port) case IPv6(ipv6, port) => new InetSocketAddress(ipv6, port) + case Tor2(tor2, port) => OnionAddress.fromParts(tor2, port).toInetSocketAddress + case Tor3(tor3, port) => OnionAddress.fromParts(tor3, port).toInetSocketAddress case _ => ??? } m += (nodeid -> nodeaddress) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 898284fc1e..26dc7b327a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -20,8 +20,8 @@ import java.net.InetSocketAddress import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{BinaryData, Crypto, LexicographicalOrdering} -import fr.acinq.eclair.{ShortChannelId, serializationResult} import fr.acinq.eclair.wire._ +import fr.acinq.eclair.{ShortChannelId, serializationResult} import scodec.bits.BitVector import shapeless.HNil @@ -75,8 +75,9 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { require(alias.size <= 32) + val nodeAddresses = addresses.map(NodeAddress(_)) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", nodeAddresses) val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte NodeAnnouncement( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index ea16dc692e..35139aab30 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -21,14 +21,14 @@ import java.io.StringWriter import akka.actor.{ActorRef, Props, Status} import akka.event.Logging.MDC import akka.pattern.pipe -import fr.acinq.bitcoin.{BinaryData, Block} +import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Peer.{ChannelClosed, NonexistingChannel, InvalidSignature, PeerRoutingMessage} +import fr.acinq.eclair.io.Peer.{ChannelClosed, InvalidSignature, NonexistingChannel, PeerRoutingMessage} import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire._ @@ -140,7 +140,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // on restart we update our node announcement // note that if we don't currently have public channels, this will be ignored - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.nodeAddresses) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) self ! nodeAnn log.info(s"initialization completed, ready to process messages") @@ -232,7 +232,7 @@ class Router(nodeParams: NodeParams, watcher: ActorRef) extends FSMDiagnosticAct // in case we just validated our first local channel, we announce the local node if (!d0.nodes.contains(nodeParams.nodeId) && isRelatedTo(c, nodeParams.nodeId)) { log.info("first local channel validated, announcing local node") - val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.nodeAddresses) + val nodeAnn = Announcements.makeNodeAnnouncement(nodeParams.privateKey, nodeParams.alias, nodeParams.color, nodeParams.publicAddresses) self ! nodeAnn } true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala index 0faff6846c..5fc02ed62a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala @@ -1,16 +1,65 @@ package fr.acinq.eclair.tor +import java.net.InetSocketAddress + import org.apache.commons.codec.binary.Base32 sealed trait OnionAddress { + import OnionAddress._ + val onionService: String - val port: Int + def getHostString: String = s"$onionService$OnionSuffix" + + val getPort: Int + + def toOnion: String = s"$getHostString:$getPort" - def toOnion: String = s"$onionService.onion:$port" + def decodedOnionService: Array[Byte] = base32decode(onionService.toUpperCase) + + def toInetSocketAddress: InetSocketAddress = new InetSocketAddress(getHostString, getPort) +} + +case class OnionAddressV2(onionService: String, getPort: Int) extends OnionAddress { + require(onionService.length == OnionAddress.v2Len) +} - def decodedOnionService: Array[Byte] = new Base32().decode(onionService.toUpperCase) +case class OnionAddressV3(onionService: String, getPort: Int) extends OnionAddress { + require(onionService.length == OnionAddress.v3Len) } -case class OnionAddressV2(onionService: String, port: Int) extends OnionAddress { require(onionService.length == 16) } -case class OnionAddressV3(onionService: String, port: Int) extends OnionAddress { require(onionService.length == 56) } +object OnionAddress { + val OnionSuffix = ".onion" + val v2Len = 16 + val v3Len = 56 + + def hostString(host: Array[Byte]): String = s"${base32encode(host)}$OnionSuffix" + + def fromParts(host: Array[Byte], port: Int): OnionAddress = { + val onionService = base32encode(host) + onionService.length match { + case `v2Len` => OnionAddressV2(onionService, port) + case `v3Len` => OnionAddressV3(onionService, port) + case _ => throw new RuntimeException(s"Invalid Tor address `$onionService`") + } + } + + def fromParts(hostname: String, port: Int): Option[OnionAddress] = if (isOnion(hostname)) { + val onionService = hostname.stripSuffix(OnionSuffix) + onionService.length match { + case `v2Len` => Some(OnionAddressV2(onionService, port)) + case `v3Len` => Some(OnionAddressV3(onionService, port)) + case _ => None + } + } else { + None + } + + def isOnion(hostname: String): Boolean = hostname.endsWith(OnionSuffix) + + def decodeHostname(hostname: String): Array[Byte] = base32decode(hostname.stripSuffix(OnionSuffix)) + + def base32decode(s: String): Array[Byte] = new Base32().decode(s.toUpperCase) + + def base32encode(a: Seq[Byte]): String = new Base32().encodeAsString(a.toArray).toLowerCase +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 2e3d368d1c..228d8a7f45 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -166,15 +166,21 @@ sealed trait NodeAddress { def getPort: Int } case object NodeAddress { - def apply(inetSocketAddress: InetSocketAddress): NodeAddress = inetSocketAddress.getAddress match { - case a: Inet4Address => IPv4(a, inetSocketAddress.getPort) - case a: Inet6Address => IPv6(a, inetSocketAddress.getPort) - case _ => throw new RuntimeException(s"Invalid socket address $inetSocketAddress") - } + def apply(inetSocketAddress: InetSocketAddress): NodeAddress = + OnionAddress.fromParts(inetSocketAddress.getHostString, inetSocketAddress.getPort) match { + case Some(OnionAddressV2(onionService, port)) => Tor2(BinaryData(OnionAddress.decodeHostname(onionService)), port) + case Some(OnionAddressV3(onionService, port)) => Tor3(BinaryData(OnionAddress.decodeHostname(onionService)), port) + case _ => + inetSocketAddress.getAddress match { + case a: Inet4Address => IPv4(a, inetSocketAddress.getPort) + case a: Inet6Address => IPv6(a, inetSocketAddress.getPort) + case _ => throw new RuntimeException(s"Invalid socket address $inetSocketAddress") + } + } def apply(onionAddress: OnionAddress): NodeAddress = { onionAddress match { - case _: OnionAddressV2 => Tor2(BinaryData(onionAddress.decodedOnionService), onionAddress.port) - case _: OnionAddressV3 => Tor3(BinaryData(onionAddress.decodedOnionService), onionAddress.port) + case _: OnionAddressV2 => Tor2(BinaryData(onionAddress.decodedOnionService), onionAddress.getPort) + case _: OnionAddressV3 => Tor3(BinaryData(onionAddress.decodedOnionService), onionAddress.getPort) } } } @@ -192,12 +198,12 @@ case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { } case class Tor2(tor2: BinaryData, port: Int) extends NodeAddress { require(tor2.size == 10) - override def getHostString: String = tor2.toString() + override def getHostString: String = OnionAddress.hostString(tor2) override def getPort: Int = port } case class Tor3(tor3: BinaryData, port: Int) extends NodeAddress { require(tor3.size == 35) - override def getHostString: String = tor3.toString() + override def getHostString: String = OnionAddress.hostString(tor3) override def getPort: Int = port } // @formatter:on @@ -212,6 +218,8 @@ case class NodeAnnouncement(signature: BinaryData, def socketAddresses: List[InetSocketAddress] = addresses.collect { case IPv4(a, port) => new InetSocketAddress(a, port) case IPv6(a, port) => new InetSocketAddress(a, port) + case Tor2(a, port) => OnionAddress.fromParts(a, port).toInetSocketAddress + case Tor3(a, port) => OnionAddress.fromParts(a, port).toInetSocketAddress } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 9ec9f58eef..80bfcacbc7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.api import java.net.{InetAddress, InetSocketAddress} import fr.acinq.bitcoin.{BinaryData, OutPoint} +import fr.acinq.eclair.tor.OnionAddress import fr.acinq.eclair.transactions.{IN, OUT} import fr.acinq.eclair.wire.NodeAddress import org.json4s.jackson.Serialization @@ -51,9 +52,13 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("NodeAddress serialization") { val ipv4 = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(10, 0, 0, 1)), 8888)) val ipv6LocalHost = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735)) + val tor2 = NodeAddress(OnionAddress.fromParts(Array.tabulate(10)(_.toByte), 7777).toInetSocketAddress) + val tor3 = NodeAddress(OnionAddress.fromParts(Array.tabulate(35)(_.toByte), 9999).toInetSocketAddress) Serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" Serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" + Serialization.write(tor2)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" + Serialization.write(tor3)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" } test("Direction serialization") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index a48628ea65..9303d78d1e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -22,7 +22,8 @@ import java.sql.DriverManager import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.wire.{Color, NodeAddress} +import fr.acinq.eclair.tor.OnionAddressV2 +import fr.acinq.eclair.wire.Color import fr.acinq.eclair.{ShortChannelId, randomKey} import org.scalatest.FunSuite import org.sqlite.SQLiteException @@ -42,9 +43,10 @@ class SqliteNetworkDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) + val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), OnionAddressV2("aaaqeayeaudaocaj", 42000).toInetSocketAddress :: Nil) assert(db.listNodes().toSet === Set.empty) db.addNode(node_1) @@ -52,10 +54,13 @@ class SqliteNetworkDbSpec extends FunSuite { assert(db.listNodes().size === 1) db.addNode(node_2) db.addNode(node_3) - assert(db.listNodes().toSet === Set(node_1, node_2, node_3)) + db.addNode(node_4) + assert(db.listNodes().toSet === Set(node_1, node_2, node_3, node_4)) db.removeNode(node_2.nodeId) - assert(db.listNodes().toSet === Set(node_1, node_3)) + assert(db.listNodes().toSet === Set(node_1, node_3, node_4)) db.updateNode(node_1) + + assert(node_4.socketAddresses == List(new InetSocketAddress("aaaqeayeaudaocaj.onion", 42000))) } test("add/remove/list channels and channel_updates") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index fda7cb193c..998b69d589 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -47,7 +47,7 @@ class AnnouncementsSpec extends FunSuite { } test("create valid signed node announcement") { - val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.nodeAddresses) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index 86e67f2019..628d263287 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -365,7 +365,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext bitcoinChain.setText(s"${setup.chain.toUpperCase()}") bitcoinChain.getStyleClass.add(setup.chain) - val nodeURI_opt = setup.nodeParams.nodeAddresses.headOption.map(address => { + val nodeURI_opt = setup.nodeParams.publicAddresses.headOption.map(address => { s"${setup.nodeParams.nodeId}@${HostAndPort.fromParts(address.getHostString, address.getPort)}" }) From af401f7a15282021792d7021edfa92f909ddae3f Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 21 Oct 2018 14:01:16 -0700 Subject: [PATCH 03/40] cleanup --- .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 4 ++-- .../main/scala/fr/acinq/eclair/PortChecker.scala | 10 +++++++--- .../src/main/scala/fr/acinq/eclair/Setup.scala | 13 ++++++++----- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 62d1f8dec5..3b431376b8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -122,7 +122,7 @@ object NodeParams { } } - def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager, publicAddress: Option[InetSocketAddress]): NodeParams = { + def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager, torAddressOpt: Option[InetSocketAddress]): NodeParams = { datadir.mkdirs() @@ -164,7 +164,7 @@ object NodeParams { keyManager = keyManager, alias = config.getString("node-alias").take(32), color = Color(color.data(0), color.data(1), color.data(2)), - publicAddresses = publicAddress.map(List(_)).getOrElse(config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port")))), + publicAddresses = torAddressOpt.map(List(_)).getOrElse(config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port")))), globalFeatures = BinaryData(config.getString("global-features")), localFeatures = BinaryData(config.getString("local-features")), dustLimitSatoshis = dustLimitSatoshis, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/PortChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/PortChecker.scala index 98213a853f..9d5c11730e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/PortChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/PortChecker.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair -import java.net.{InetAddress, ServerSocket} +import java.net.{InetAddress, InetSocketAddress, ServerSocket} import scala.util.{Failure, Success, Try} @@ -28,8 +28,12 @@ object PortChecker { * * @return */ - def checkAvailable(host: String, port: Int): Unit = { - Try(new ServerSocket(port, 50, InetAddress.getByName(host))) match { + def checkAvailable(host: String, port: Int): Unit = checkAvailable(InetAddress.getByName(host), port) + + def checkAvailable(socketAddress: InetSocketAddress): Unit = checkAvailable(socketAddress.getAddress, socketAddress.getPort) + + def checkAvailable(address: InetAddress, port: Int): Unit = { + Try(new ServerSocket(port, 50, address)) match { case Success(socket) => Try(socket.close()) case Failure(_) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 0e4167e228..5ee8e055ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -77,14 +77,17 @@ class Setup(datadir: File, // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) secureRandom.nextInt() - val torPublicAddress = initTor() + val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, initTor()) - val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, torPublicAddress) + // always bind the server to localhost when using Tor + val serverBindingAddress = new InetSocketAddress( + if (config.getBoolean("tor.enabled")) "127.0.0.1" else config.getString("server.binding-ip"), + config.getInt("server.port")) // early checks DBCompatChecker.checkDBCompatibility(nodeParams) DBCompatChecker.checkNetworkDBCompatibility(nodeParams) - PortChecker.checkAvailable(config.getString("server.binding-ip"), config.getInt("server.port")) + PortChecker.checkAvailable(serverBindingAddress) logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") @@ -199,7 +202,7 @@ class Setup(datadir: File, router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume)) authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) - server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")), Some(tcpBound)), "server", SupervisorStrategy.Restart)) + server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart)) kit = Kit( @@ -282,7 +285,7 @@ class Setup(datadir: File, val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") logger.info(s"Tor address ${torAddress.toOnion}") - Option(torAddress.toInetSocketAddress) + Some(torAddress.toInetSocketAddress) } else { None } From 1daca47a8ee9bd4251387d0289082c0f8b98afbd Mon Sep 17 00:00:00 2001 From: rorp Date: Sat, 3 Nov 2018 22:34:08 -0700 Subject: [PATCH 04/40] SOCKS5 --- .../main/scala/fr/acinq/eclair/Setup.scala | 19 +- .../scala/fr/acinq/eclair/io/Client.scala | 60 ++++- .../main/scala/fr/acinq/eclair/io/Peer.scala | 8 +- .../fr/acinq/eclair/io/Switchboard.scala | 8 +- .../fr/acinq/eclair/tor/Controller.scala | 6 +- .../acinq/eclair/tor/Socks5Connection.scala | 169 ++++++++++++++ .../eclair/crypto/TransportHandlerSpec.scala | 1 + .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- .../eclair/tor/TorProtocolHandlerSpec.scala | 217 +++++++++++++++++- 9 files changed, 453 insertions(+), 37 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 5ee8e055ff..348dc6cbf0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -62,7 +62,9 @@ class Setup(datadir: File, logger.info(s"hello!") logger.info(s"version=${getClass.getPackage.getImplementationVersion} commit=${getClass.getPackage.getSpecificationVersion}") logger.info(s"datadir=${datadir.getCanonicalPath}") - + logger.info(s"initializing secure random generator") + // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) + secureRandom.nextInt() val config = NodeParams.loadConfiguration(datadir, overrideDefaults) val seed = seed_opt.getOrElse(NodeParams.getSeed(datadir)) @@ -73,12 +75,11 @@ class Setup(datadir: File, implicit val formats = org.json4s.DefaultFormats implicit val ec = ExecutionContext.Implicits.global - logger.info(s"initializing secure random generator") - // this will force the secure random instance to initialize itself right now, making sure it doesn't hang later (see comment in package.scala) - secureRandom.nextInt() - val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, initTor()) + logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") + logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") + // always bind the server to localhost when using Tor val serverBindingAddress = new InetSocketAddress( if (config.getBoolean("tor.enabled")) "127.0.0.1" else config.getString("server.binding-ip"), @@ -89,9 +90,6 @@ class Setup(datadir: File, DBCompatChecker.checkNetworkDBCompatibility(nodeParams) PortChecker.checkAvailable(serverBindingAddress) - logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") - logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") - val bitcoin = nodeParams.watcherType match { case BITCOIND => val bitcoinClient = new BasicBitcoinJsonRPCClient( @@ -201,7 +199,8 @@ class Setup(datadir: File, relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume)) router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume)) authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) - switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) + socksProxy = if (config.getBoolean("tor.enabled")) Some(new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.socks-port"))) else None + switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart)) @@ -267,8 +266,6 @@ class Setup(datadir: File, private def initTor(): Option[InetSocketAddress] = { if (config.getBoolean("tor.enabled")) { - sys.props.put("socksProxyHost", config.getString("tor.host")) - sys.props.put("socksProxyPort", config.getInt("tor.socks-port").toString) if (config.getString("tor.protocol") != "socks") { val promiseTorAddress = Promise[OnionAddress]() val protocolHandler = system.actorOf(TorProtocolHandler.props( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 657d10de02..b959dda094 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -23,8 +23,10 @@ import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.{Logs, NodeParams} import fr.acinq.eclair.io.Client.ConnectionFailed +import fr.acinq.eclair.tor.Socks5Connection +import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected} +import fr.acinq.eclair.{Logs, NodeParams} import scala.concurrent.duration._ @@ -32,7 +34,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[InetSocketAddress]) extends Actor with DiagnosticActorLogging { import Tcp._ import context.system @@ -40,22 +42,58 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke // we could connect directly here but this allows to take advantage of the automated mdc configuration on message reception self ! 'connect + private var connection: ActorRef = _ + def receive = { + case 'connect => - log.info(s"connecting to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort}") - IO(Tcp) ! Connect(address, timeout = Some(5 seconds), options = KeepAlive(true) :: Nil, pullMode = true) + val addressToConnect = socksProxy match { + case None => + log.info(s"connecting to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort}") + address + case Some(socksProxyAddress) => + log.info(s"connecting to SOCKS5 proxy $socksProxyAddress") + socksProxyAddress + } + IO(Tcp) ! Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) case CommandFailed(_: Connect) => - log.info(s"connection failed to $remoteNodeId@${address.getHostString}:${address.getPort}") + socksProxy match { + case None => + log.info(s"connection failed to $remoteNodeId@${address.getHostString}:${address.getPort}") + case Some(socksProxyAddress) => + log.info(s"connection failed to SOCKS5 proxy $socksProxyAddress") + } + origin_opt.map(_ ! Status.Failure(ConnectionFailed(address))) + context stop self + + case CommandFailed(_: Socks5Connect) => + log.info(s"connection failed to $remoteNodeId@${address.getHostString}:${address.getPort} via SOCKS5 ${socksProxy.map(_.toString).getOrElse("")}") origin_opt.map(_ ! Status.Failure(ConnectionFailed(address))) context stop self case Connected(remote, _) => - log.info(s"connected to pubkey=$remoteNodeId host=${remote.getHostString} port=${remote.getPort}") - val connection = sender - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) - context watch connection - context become connected(connection) + socksProxy match { + case None => + connection = sender() + context watch connection + log.info(s"connected to pubkey=$remoteNodeId host=${remote.getHostString} port=${remote.getPort}") + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) + context become connected(connection) + case Some(_) => + connection = context.actorOf(Socks5Connection.props(sender())) + context watch connection + connection ! Socks5Connect(address) + } + + case Socks5Connected(_) => + socksProxy match { + case Some(proxyAddress) => + log.info(s"connected to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort} via SOCKS5 proxy $proxyAddress") + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) + context become connected(connection) + case _ => + } } def connected(connection: ActorRef): Receive = { @@ -70,7 +108,7 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke object Client extends App { - def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) + def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[InetSocketAddress]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt, socksProxy)) case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 685410d967..c0d96641c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -38,7 +38,7 @@ import scala.util.Random /** * Created by PM on 26/08/2016. */ -class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { +class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { import Peer._ @@ -57,7 +57,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(DISCONNECTED) { case Event(Peer.Connect(NodeURI(_, address)), _) => // even if we are in a reconnection loop, we immediately process explicit connection requests - context.actorOf(Client.props(nodeParams, authenticator, new InetSocketAddress(address.getHost, address.getPort), remoteNodeId, origin_opt = Some(sender()))) + context.actorOf(Client.props(nodeParams, authenticator, new InetSocketAddress(address.getHost, address.getPort), remoteNodeId, origin_opt = Some(sender()), socksProxy)) stay case Event(Reconnect, d@DisconnectedData(address_opt, channels, attempts)) => @@ -65,7 +65,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor case None => stay // no-op (this peer didn't initiate the connection and doesn't have the ip of the counterparty) case _ if channels.isEmpty => stay // no-op (no more channels with this peer) case Some(address) => - context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = None)) + context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = None, socksProxy)) // exponential backoff retry with a finite max setTimer(RECONNECT_TIMER, Reconnect, Math.min(10 + Math.pow(2, attempts), 60) seconds, repeat = false) stay using d.copy(attempts = attempts + 1) @@ -431,7 +431,7 @@ object Peer { val IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD = 5 minutes - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet: EclairWallet)) + def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy)) // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index cb14ac442b..623a35e8ee 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress +import java.net.{InetAddress, InetSocketAddress} import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated} import fr.acinq.bitcoin.Crypto.PublicKey @@ -29,7 +29,7 @@ import fr.acinq.eclair.router.Rebroadcast * Ties network connections to peers. * Created by PM on 14/02/2017. */ -class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends Actor with ActorLogging { +class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) extends Actor with ActorLogging { authenticator ! self @@ -101,7 +101,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto case Some(peer) => peer case None => log.info(s"creating new peer current=${peers.size}") - val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet), name = s"peer-$remoteNodeId") + val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy), name = s"peer-$remoteNodeId") peer ! Peer.Init(previousKnownAddress, offlineChannels) context watch (peer) peer @@ -116,6 +116,6 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto object Switchboard { - def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet)) + def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy)) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala index 7e744af8d4..661aa0385b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.tor import java.net.InetSocketAddress -import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} import akka.io.{IO, Tcp} import akka.util.ByteString @@ -29,6 +29,7 @@ class Controller(address: InetSocketAddress, listener: ActorRef) listener ! c val connection = sender() connection ! Register(self) + context watch connection context become { case data: ByteString => connection ! Write(data) @@ -41,6 +42,9 @@ class Controller(address: InetSocketAddress, listener: ActorRef) case _: ConnectionClosed => context stop listener context stop self + case Terminated(actor) if actor == connection => + context stop listener + context stop self } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala new file mode 100644 index 0000000000..bc2d89d91c --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -0,0 +1,169 @@ +package fr.acinq.eclair.tor + +import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} + +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} +import akka.io.Tcp +import akka.util.ByteString + +class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { + + import fr.acinq.eclair.tor.Socks5Connection._ + + private var handler: ActorRef = _ + + context watch underlying + + override def receive: Receive = { + case c@Socks5Connect(address) => + context become greetings(sender(), c) + underlying ! Tcp.Register(self) + underlying ! Tcp.ResumeReading + log.info(s"send greeting") + underlying ! Tcp.Write(socks5Greeting) + } + + def greetings(commander: ActorRef, connectCommand: Socks5Connect): Receive = { + case Tcp.Received(data) => + log.info(s"receive auth") + try { + if (data(0) != 0x05) { + throw new RuntimeException("Invalid SOCKS5 proxy response") + } else if (data(1) != 0x00) { + throw new RuntimeException("Unrecognized SOCKS5 auth method") + } else { + context become connectionRequest(commander, connectCommand) + log.info(s"send con req") + underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) + underlying ! Tcp.ResumeReading + } + } catch { + case e: Throwable => handleErrors("Error connecting to SOCKS5 proxy", commander, connectCommand, e) + } + case c: Tcp.ConnectionClosed => + commander ! c + context stop self + } + + def connectionRequest(commander: ActorRef, connectCommand: Socks5Connect): Receive = { + case c@Tcp.Received(data) => + log.info(s"receive address") + try { + if (data(0) != 0x05) { + throw new RuntimeException("Invalid SOCKS5 proxy response") + } else { + val status = data(1) + if (status != 0) { + throw new RuntimeException(connectErrors.getOrElse(status, s"Unknown SOCKS5 error $status")) + } + val connectedAddress = data(3) match { + case 0x01 => + val ip = Array(data(4), data(5), data(6), data(7)) + val port = data(8).toInt << 8 | data(9) + new InetSocketAddress(InetAddress.getByAddress(ip), port) + case 0x03 => + val len = data(4) + val start = 5 + val end = start + len + val domain = data.slice(start, end).utf8String + val port = data(end).toInt << 8 | data(end + 1) + new InetSocketAddress(domain, port) + case 0x04 => + val ip = Array.ofDim[Byte](16) + data.copyToArray(ip, 4, 4 + ip.length) + val port = data(4 + ip.length).toInt << 8 | data(4 + ip.length + 1) + new InetSocketAddress(InetAddress.getByAddress(ip), port) + case _ => throw new RuntimeException(s"Unrecognized address type") + } + context become connected + log.info(s"connected $connectedAddress") + commander ! Socks5Connected(connectedAddress) + } + } catch { + case e: Throwable => handleErrors("Cannot establish SOCKS5 connection", commander, connectCommand, e) + } + case c: Tcp.ConnectionClosed => + commander ! c + context stop self + } + + def connected: Receive = { + case Tcp.Register(actor, keepOpenOnPeerClosed, useResumeWriting) => + handler = actor + context become registered + } + + def registered: Receive = { + case c: Tcp.Command => underlying ! c + case e: Tcp.Event => handler ! e + } + + override def unhandled(message: Any): Unit = message match { + case Terminated(actor) if actor == underlying => context stop self + case _ => log.warning(s"unhandled message=$message") + } + + private def handleErrors(message: String, commander: ActorRef, connectCommand: Socks5Connect, e: Throwable): Unit = { + log.error(e, message + " ") + underlying ! Tcp.Close + commander ! connectCommand.failureMessage + } +} + +object Socks5Connection { + def props(tcpConnection: ActorRef): Props = Props(new Socks5Connection(tcpConnection)) + + case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event + + case class Socks5Connect(address: InetSocketAddress) extends Tcp.Command + + val connectErrors: Map[Byte, String] = Map[Byte, String]( + (0x00, "Request granted"), + (0x01, "General failure"), + (0x02, "Connection not allowed by ruleset"), + (0x03, "Network unreachable"), + (0x04, "Host unreachable"), + (0x05, "Connection refused by destination host"), + (0x06, "TTL expired"), + (0x07, "Command not supported / protocol error"), + (0x08, "Address type not supported") + ) + + val socks5Greeting = ByteString( + 0x05, // SOCKS version + 0x01, // number of authentication methods supported + 0x00) // reserved + + def socks5ConnectionRequest(address: InetSocketAddress): ByteString = { + ByteString( + 0x05, // SOCKS version + 0x01, // establish a TCP/IP stream connection + 0x00) ++ // reserved + addressToByteString(address) ++ + portToByteString(address.getPort) + } + + def inetAddressToByteString(inet: InetAddress): ByteString = inet match { + case a: Inet4Address => ByteString( + 0x01 // IPv4 address + ) ++ ByteString(a.getAddress) + case a: Inet6Address => ByteString( + 0x04 // IPv6 address + ) ++ ByteString(a.getAddress) + case _ => throw new RuntimeException("Unknown InetAddress") + } + + def addressToByteString(address: InetSocketAddress): ByteString = Option(address.getAddress) match { + case None => + // unresolved address, use SOCKS5 resolver + val host = address.getHostString + ByteString( + 0x03, // Domain name + host.length.toByte) ++ + ByteString(host) + case Some(inetAddress) => + inetAddressToByteString(inetAddress) + } + + def portToByteString(port: Int): ByteString = ByteString((port & 0x0000ff00) >> 8, port & 0x000000ff) +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala index c951565c2b..cd0ed6a847 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/TransportHandlerSpec.scala @@ -16,6 +16,7 @@ package fr.acinq.eclair.crypto +import java.net.InetSocketAddress import java.nio.charset.Charset import akka.actor.{Actor, ActorLogging, ActorRef, ActorSystem, OneForOneStrategy, Props, Stash, SupervisorStrategy, Terminated} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index a0b196985f..7f2cbf6261 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -35,7 +35,7 @@ class PeerSpec extends TestkitBaseClass { val transport = TestProbe() val wallet: EclairWallet = null // unused val remoteNodeId = Bob.nodeParams.nodeId - val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) + val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet, None)) withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index da3956e5c7..d38fe1c01c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -1,9 +1,11 @@ package fr.acinq.eclair.tor import java.io.{File, IOException} -import java.net.InetSocketAddress +import java.net.{InetAddress, InetSocketAddress, Socket} +import java.nio.charset.Charset import java.nio.file.attribute.BasicFileAttributes import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} +import java.util.Date import akka.actor.ActorSystem import akka.actor.Status.Failure @@ -33,15 +35,218 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val ClientNonce = "8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA" val AuthCookie = "AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485" - "tor" ignore { + "tor" in { sys.props.put("socksProxyHost", "localhost") sys.props.put("socksProxyPort", "9050") -// val u = new URL("http://bitcoin.org/") + val socksProxy = new InetSocketAddress(sys.props("socksProxyHost"), sys.props("socksProxyPort").toInt) + val host = "hqa6yepjl2uwzn2y.onion" + val port = 9735 +// +// +// class ClientHandler extends ChannelInboundHandlerAdapter { +// +// val greetings = Unpooled.buffer(3) +// greetings.writeByte(0x05) +// greetings.writeByte(0x01) +// greetings.writeByte(0x00) +// +// val connReq = Unpooled.buffer(host.length + 6) +// connReq.writeByte(0x05) +// connReq.writeByte(0x01) +// connReq.writeByte(0x00) +// connReq.writeByte(0x03) +// connReq.writeByte(host.length.toByte) +// connReq.writeCharSequence(host, Charset.forName("UTF-8")) +// connReq.writeByte(port.toByte) +// connReq.writeByte((port >> 8).toByte) +// +// println(Integer.toHexString(port)) +// +// sealed trait State +// case object Init extends State +// case object Error extends State +// case object Authed extends State +// case object Sent extends State +// +// @volatile var state: State = Init +// +// +// override def channelActive(ctx: ChannelHandlerContext): Unit = { +// println("channelActive") +// ctx.writeAndFlush(greetings) +// } +// +// override def channelInactive(ctx: ChannelHandlerContext): Unit = { +// println("channelInactive") +// } +// +// override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { +// println("exceptionCaught") +// cause.printStackTrace() +// ctx.close() +// } +// +// override def channelReadComplete(ctx: ChannelHandlerContext): Unit = { +// println("channelReadComplete") +// state match { +// case Authed => +// ctx.writeAndFlush(connReq) +// state = Sent +// case x@_ => +// println(x) +// } +// } +// +// override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = { +// println("channelRead") +// val m = msg.asInstanceOf[ByteBuf] // (1) +// println(m.readableBytes()) +// try { +// state match { +// case Init => +// if (m.readByte() != 0x05) { +// throw new RuntimeException() +// } +// if (m.readByte() != 0x00) { +// throw new RuntimeException() +// } +// state = Authed +// case Sent => +// if (m.readByte() != 0x05) { +// throw new RuntimeException() +// } +// val x = m.readByte() +// println(x) +// if (x != 0x00) { +// //throw new RuntimeException() +// } +// val y = m.readByte() +// println(y) +// val z = m.readByte() +// println(z) +// if (z != 0x01) { +// //throw new RuntimeException() +// } +// val ip = Array(m.readByte(), m.readByte(), m.readByte(), m.readByte()) +// val addr = InetAddress.getByAddress(ip) +// +// val p = m.readByte().toInt << 8 | m.readByte() +// println(addr) +// println(p) +// +// val buf = Unpooled.buffer +// buf.writeCharSequence(host, Charset.forName("UTF-8")) +// ctx.writeAndFlush(buf) +// +// //ctx.close() +// case x@_ => +// println(x) +// } +// } catch { +// case e: Throwable => +// e.printStackTrace() +// state = Error +// } finally { +// m.release +// } +// } +// } +// + +// val workerGroup: EventLoopGroup = new NioEventLoopGroup +// try { +// val b = new Bootstrap() // (1) +// b.group(workerGroup) // (2) +// b.channel(classOf[NioSocketChannel]) // (3) +// b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) // (4) +// b.handler(new ChannelInitializer[SocketChannel] { +// override def initChannel(ch: SocketChannel): Unit = { +//// ch.pipeline.addFirst(new Socks5ProxyHandler(socksProxy, "uuuuuuuu", "pppppppp")) +// ch.pipeline.addLast(new ClientHandler()) +// } +// }) +// // Start the client. +// val f = b.connect(socksProxy).sync() // (5) +// +// // Wait until the connection is closed. +// f.channel().closeFuture().sync() +// } finally workerGroup.shutdownGracefully() + + /* + val workerGroup: EventLoopGroup = new NioEventLoopGroup + try { + val b = new Bootstrap() // (1) + b.group(workerGroup) // (2) + b.channel(classOf[NioSocketChannel]) // (3) + b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) // (4) + b.handler(new ChannelInitializer[SocketChannel] { + override def initChannel(ch: SocketChannel): Unit = { + ch.pipeline.addLast(new ClientHandler()) + } + }) + // Start the client. + val f = b.connect(socksProxy).sync() // (5) + + // Wait until the connection is closed. + f.channel().closeFuture().sync() + } finally workerGroup.shutdownGracefully() +*/ + // val channel = SocketChannel.open(socksProxy) +// val b = ByteBuffer.allocate(3) +// b.put(0x05.toByte) +// b.put(0x01.toByte) +// b.put(0x00.toByte) +// channel.write(b) +// val b1 = ByteBuffer.allocate(2) +// channel.read(b1) +// println(b1) + + +// val promiseInetAddress = Some(Promise[InetSocketAddress]()) + +// val facilitator = TestActorRef(SocksConnectionFacilitator.props(promiseInetAddress), "socks") +// val controller = TestActorRef(Controller.props(socksProxy, facilitator, Some(new InetSocketAddress(host, port))), "controller") + +// expectMsg(125 seconds, SocksConnected(null)) +// val inet = Await.result(promiseInetAddress.future, 125 seconds) +// println(inet) + +// val workerGroup = new NioEventLoopGroup() +// try { +// val b = new Bootstrap() +// b.group(workerGroup) +// b.channel(classOf[NioSocketChannel]) +// b.option(ChannelOption.SO_KEEPALIVE, true) + +// b.handler(new ChannelInitializer[SocketChannel] { +// override def initChannel(ch: SocketChannel): Unit = { +// println("initChannel") +//// ch.pipeline().addFirst(new Socks5ProxyHandler(new InetSocketAddress(sys.props("socksProxyHost"), sys.props("socksProxyPort").toInt))) +//// ch.pipeline.addLast(new Nothing) +// } +// }) + +// val isa = new InetSocketAddress("hqa6yepjl2uwzn2y.onion", 9735) +// val ia = isa.getAddress +// println(ia) + +// val f = b.connect("hqa6yepjl2uwzn2y.onion", 9735).sync +//// val f = b.connect("bitcoin.org", 80).sync +// f.channel().closeFuture().sync() +// } finally +// workerGroup.shutdownGracefully() + + + + // val socketChannel = java.nio.channels.SocketChannel.open() +// socketChannel.connect(new InetSocketAddress("hqa6yepjl2uwzn2y.onion", 9735)) + +// val u = new java.net.URL("http://hqa6yepjl2uwzn2y.onion:9735/") // val c = u.openConnection() -// val r = new BufferedReader(new InputStreamReader(c.getInputStream)) +// val r = new java.io.BufferedReader(new java.io.InputStreamReader(c.getInputStream)) // try { -// r.lines().forEach(new Consumer[String] { +// r.lines().forEach(new java.util.function.Consumer[String] { // override def accept(t: String): Unit = { // println(t) // } @@ -50,6 +255,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) // r.close() // } +/* val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(TorProtocolHandler.props( @@ -67,6 +273,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) println(address.onionService.length) println((address.onionService + ".onion").length) println(NodeAddress(address)) + */ } "happy path v2" in { From c35a646c2025d12ffce0e651acd5e6a95841df76 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 4 Nov 2018 14:05:00 -0800 Subject: [PATCH 05/40] docs --- TOR.md | 69 +++++++++++++++++++ .../acinq/eclair/tor/Socks5Connection.scala | 4 -- 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 TOR.md diff --git a/TOR.md b/TOR.md new file mode 100644 index 0000000000..c519de20fe --- /dev/null +++ b/TOR.md @@ -0,0 +1,69 @@ +## How to Use Tor with Eclair + +### Installing Tor on your node + +For Linux: + +```shell +sudo apt install tor +``` + +For Mac OS X: + +```shell +brew install tor +``` + +Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X) +eclair requires for safe cooke authentication as well as SOCKS5 and control connections to be enabled. +Change value of ExitPolicy parameter only if you really know what you are doing. + + +``` +SOCKSPort 9050 +ControlPort 9051 +CookieAuthentication 1 +ExitPolicy reject *:* +``` + +Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`) + +### Start Tor + +For Linux: + +```shell +sudo systemctl start tor +``` + +For Mac OS X: + +```shell +brew services start tor +``` + +### Configure eclair to use Tor + +To enable Tor support simply set `eclair.tor.enabled` parameter in `eclair.conf` to true. + +``` +eclair.tor.enabled = true +``` + +By default all traffic will be forwarded through Tor network. Note that in this case the value of `eclair.server.public-ip` +will be ignored and incoming connections will be disabled. To enable incoming connections you +need to configure Tor hidden service using `eclair.tor.protocol-version` parameter. + +``` +eclair.tor.protocol-version = "v3" +``` + +There are three possible values for protocol-version: + +value | description +-------|--------------------------------------------------------- + socks | no incoming connections allowed. + v2 | eclair sets up a Tor hidden service version 2 end point + v3 | eclair sets up a Tor hidden service version 3 + +Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index bc2d89d91c..fdeb823a8e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -19,13 +19,11 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { context become greetings(sender(), c) underlying ! Tcp.Register(self) underlying ! Tcp.ResumeReading - log.info(s"send greeting") underlying ! Tcp.Write(socks5Greeting) } def greetings(commander: ActorRef, connectCommand: Socks5Connect): Receive = { case Tcp.Received(data) => - log.info(s"receive auth") try { if (data(0) != 0x05) { throw new RuntimeException("Invalid SOCKS5 proxy response") @@ -33,7 +31,6 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { throw new RuntimeException("Unrecognized SOCKS5 auth method") } else { context become connectionRequest(commander, connectCommand) - log.info(s"send con req") underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) underlying ! Tcp.ResumeReading } @@ -47,7 +44,6 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { def connectionRequest(commander: ActorRef, connectCommand: Socks5Connect): Receive = { case c@Tcp.Received(data) => - log.info(s"receive address") try { if (data(0) != 0x05) { throw new RuntimeException("Invalid SOCKS5 proxy response") From b82dacdd4f136ed0fe35efbc4cd3f3f651461f60 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 4 Nov 2018 14:09:07 -0800 Subject: [PATCH 06/40] minor changes --- TOR.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TOR.md b/TOR.md index c519de20fe..8353db2643 100644 --- a/TOR.md +++ b/TOR.md @@ -16,7 +16,7 @@ brew install tor Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X) eclair requires for safe cooke authentication as well as SOCKS5 and control connections to be enabled. -Change value of ExitPolicy parameter only if you really know what you are doing. +Change value of `ExitPolicy` parameter only if you really know what you are doing. ``` @@ -58,12 +58,12 @@ need to configure Tor hidden service using `eclair.tor.protocol-version` paramet eclair.tor.protocol-version = "v3" ``` -There are three possible values for protocol-version: +There are three possible values for `protocol-version`: value | description -------|--------------------------------------------------------- socks | no incoming connections allowed. v2 | eclair sets up a Tor hidden service version 2 end point - v3 | eclair sets up a Tor hidden service version 3 + v3 | eclair sets up a Tor hidden service version 3 end point Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file From a3663e86844a550cb968e4c3d16ed40840bd2cb3 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 4 Nov 2018 14:35:18 -0800 Subject: [PATCH 07/40] cleanup --- .../acinq/eclair/tor/TorProtocolHandler.scala | 53 +--- .../eclair/tor/TorProtocolHandlerSpec.scala | 226 +----------------- 2 files changed, 16 insertions(+), 263 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 816ec48497..085cf6e0a8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -2,19 +2,17 @@ package fr.acinq.eclair.tor import java.io._ import java.nio.file.{Files, Paths} -import java.security.SecureRandom import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash, Status} import akka.io.Tcp.Connected import akka.util.ByteString -import fr.acinq.eclair.tor.TorProtocolHandler.ProtocolVersion import fr.acinq.eclair.randomBytes +import fr.acinq.eclair.tor.TorProtocolHandler.ProtocolVersion import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import javax.xml.bind.DatatypeConverter import scala.concurrent.Promise -import scala.util.Random case class TorException(msg: String) extends RuntimeException(msg) @@ -33,11 +31,6 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, private var receiver: ActorRef = _ - private var protoInfo: ProtocolInfo = _ - private var clientHash: Array[Byte] = _ - private var keyStr: String = _ - private var portStr: String = _ - private var address: Option[OnionAddress] = None private val nonce: Array[Byte] = clientNonce.getOrElse(randomBytes(32)) @@ -45,16 +38,14 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, override def receive: Receive = { case Connected(_, _) => receiver = sender() + sendCommand("PROTOCOLINFO 1") context become protocolInfo - self ! SendNextCommand } def protocolInfo: Receive = { - case SendNextCommand => - sendCommand("PROTOCOLINFO 1") case data: ByteString => handleExceptions { val res = parseResponse(readResponse(data)) - protoInfo = ProtocolInfo( + val protoInfo = ProtocolInfo( methods = res.getOrElse("METHODS", throw TorException("Tor auth methods not found")), cookieFile = unquote(res.getOrElse("COOKIEFILE", throw TorException("Tor cookie file not found"))), version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found")))) @@ -62,41 +53,33 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, if (!protocolVersion.supportedBy(protoInfo.version)) { throw TorException(s"Tor version ${protoInfo.version} does not support protocol $protocolVersion") } - context become authChallenge - self ! SendNextCommand + sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") + context become authChallenge(protoInfo.cookieFile) } } - def authChallenge: Receive = { - case SendNextCommand => - sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") + def authChallenge(cookieFile: String): Receive = { case data: ByteString => handleExceptions { val res = parseResponse(readResponse(data)) - clientHash = computeClientHash( + val clientHash = computeClientHash( res.getOrElse("SERVERHASH", throw TorException("Tor server hash not found")), - res.getOrElse("SERVERNONCE", throw TorException("Tor server nonce not found")) + res.getOrElse("SERVERNONCE", throw TorException("Tor server nonce not found")), + cookieFile ) + sendCommand(s"AUTHENTICATE ${hex(clientHash)}") context become authenticate - self ! SendNextCommand } } def authenticate: Receive = { - case SendNextCommand => - sendCommand(s"AUTHENTICATE ${hex(clientHash)}") case data: ByteString => handleExceptions { readResponse(data) - keyStr = computeKey - portStr = computePort + sendCommand(s"ADD_ONION $computeKey $computePort") context become addOnion - self ! SendNextCommand } } def addOnion: Receive = { - case SendNextCommand => - val cmd = s"ADD_ONION $keyStr $portStr" - sendCommand(cmd) case data: ByteString => handleExceptions { val res = readResponse(data) if (ok(res)) { @@ -150,7 +133,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, } } - private def computeClientHash(serverHash: String, serverNonce: String): Array[Byte] = { + private def computeClientHash(serverHash: String, serverNonce: String, cookieFile: String): Array[Byte] = { val decodedServerHash = unhex(serverHash) if (decodedServerHash.length != 32) throw TorException("Invalid server hash length") @@ -159,7 +142,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, if (decodedServerNonce.length != 32) throw TorException("Invalid server nonce length") - val cookie = readBytes(protoInfo.cookieFile, 32) + val cookie = readBytes(cookieFile, 32) val message = cookie ++ nonce ++ decodedServerNonce @@ -208,18 +191,8 @@ object TorProtocolHandler { } } - case object SendNextCommand - case object GetOnionAddress - case object AuthCompleted - - case object AuthFailed - - case class OnionAdded(onionAddress: OnionAddress) - - case class Error(ex: Throwable) - case class ProtocolInfo(methods: String, cookieFile: String, version: String) def readBytes(filename: String, n: Int): Array[Byte] = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index d38fe1c01c..d18f0cd37e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -35,227 +35,8 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val ClientNonce = "8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA" val AuthCookie = "AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485" - "tor" in { - sys.props.put("socksProxyHost", "localhost") - sys.props.put("socksProxyPort", "9050") - - val socksProxy = new InetSocketAddress(sys.props("socksProxyHost"), sys.props("socksProxyPort").toInt) - val host = "hqa6yepjl2uwzn2y.onion" - val port = 9735 -// -// -// class ClientHandler extends ChannelInboundHandlerAdapter { -// -// val greetings = Unpooled.buffer(3) -// greetings.writeByte(0x05) -// greetings.writeByte(0x01) -// greetings.writeByte(0x00) -// -// val connReq = Unpooled.buffer(host.length + 6) -// connReq.writeByte(0x05) -// connReq.writeByte(0x01) -// connReq.writeByte(0x00) -// connReq.writeByte(0x03) -// connReq.writeByte(host.length.toByte) -// connReq.writeCharSequence(host, Charset.forName("UTF-8")) -// connReq.writeByte(port.toByte) -// connReq.writeByte((port >> 8).toByte) -// -// println(Integer.toHexString(port)) -// -// sealed trait State -// case object Init extends State -// case object Error extends State -// case object Authed extends State -// case object Sent extends State -// -// @volatile var state: State = Init -// -// -// override def channelActive(ctx: ChannelHandlerContext): Unit = { -// println("channelActive") -// ctx.writeAndFlush(greetings) -// } -// -// override def channelInactive(ctx: ChannelHandlerContext): Unit = { -// println("channelInactive") -// } -// -// override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { -// println("exceptionCaught") -// cause.printStackTrace() -// ctx.close() -// } -// -// override def channelReadComplete(ctx: ChannelHandlerContext): Unit = { -// println("channelReadComplete") -// state match { -// case Authed => -// ctx.writeAndFlush(connReq) -// state = Sent -// case x@_ => -// println(x) -// } -// } -// -// override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = { -// println("channelRead") -// val m = msg.asInstanceOf[ByteBuf] // (1) -// println(m.readableBytes()) -// try { -// state match { -// case Init => -// if (m.readByte() != 0x05) { -// throw new RuntimeException() -// } -// if (m.readByte() != 0x00) { -// throw new RuntimeException() -// } -// state = Authed -// case Sent => -// if (m.readByte() != 0x05) { -// throw new RuntimeException() -// } -// val x = m.readByte() -// println(x) -// if (x != 0x00) { -// //throw new RuntimeException() -// } -// val y = m.readByte() -// println(y) -// val z = m.readByte() -// println(z) -// if (z != 0x01) { -// //throw new RuntimeException() -// } -// val ip = Array(m.readByte(), m.readByte(), m.readByte(), m.readByte()) -// val addr = InetAddress.getByAddress(ip) -// -// val p = m.readByte().toInt << 8 | m.readByte() -// println(addr) -// println(p) -// -// val buf = Unpooled.buffer -// buf.writeCharSequence(host, Charset.forName("UTF-8")) -// ctx.writeAndFlush(buf) -// -// //ctx.close() -// case x@_ => -// println(x) -// } -// } catch { -// case e: Throwable => -// e.printStackTrace() -// state = Error -// } finally { -// m.release -// } -// } -// } -// - -// val workerGroup: EventLoopGroup = new NioEventLoopGroup -// try { -// val b = new Bootstrap() // (1) -// b.group(workerGroup) // (2) -// b.channel(classOf[NioSocketChannel]) // (3) -// b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) // (4) -// b.handler(new ChannelInitializer[SocketChannel] { -// override def initChannel(ch: SocketChannel): Unit = { -//// ch.pipeline.addFirst(new Socks5ProxyHandler(socksProxy, "uuuuuuuu", "pppppppp")) -// ch.pipeline.addLast(new ClientHandler()) -// } -// }) -// // Start the client. -// val f = b.connect(socksProxy).sync() // (5) -// -// // Wait until the connection is closed. -// f.channel().closeFuture().sync() -// } finally workerGroup.shutdownGracefully() - - /* - val workerGroup: EventLoopGroup = new NioEventLoopGroup - try { - val b = new Bootstrap() // (1) - b.group(workerGroup) // (2) - b.channel(classOf[NioSocketChannel]) // (3) - b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) // (4) - b.handler(new ChannelInitializer[SocketChannel] { - override def initChannel(ch: SocketChannel): Unit = { - ch.pipeline.addLast(new ClientHandler()) - } - }) - // Start the client. - val f = b.connect(socksProxy).sync() // (5) - - // Wait until the connection is closed. - f.channel().closeFuture().sync() - } finally workerGroup.shutdownGracefully() -*/ - // val channel = SocketChannel.open(socksProxy) -// val b = ByteBuffer.allocate(3) -// b.put(0x05.toByte) -// b.put(0x01.toByte) -// b.put(0x00.toByte) -// channel.write(b) -// val b1 = ByteBuffer.allocate(2) -// channel.read(b1) -// println(b1) - - -// val promiseInetAddress = Some(Promise[InetSocketAddress]()) - -// val facilitator = TestActorRef(SocksConnectionFacilitator.props(promiseInetAddress), "socks") -// val controller = TestActorRef(Controller.props(socksProxy, facilitator, Some(new InetSocketAddress(host, port))), "controller") - -// expectMsg(125 seconds, SocksConnected(null)) -// val inet = Await.result(promiseInetAddress.future, 125 seconds) -// println(inet) - -// val workerGroup = new NioEventLoopGroup() -// try { -// val b = new Bootstrap() -// b.group(workerGroup) -// b.channel(classOf[NioSocketChannel]) -// b.option(ChannelOption.SO_KEEPALIVE, true) - -// b.handler(new ChannelInitializer[SocketChannel] { -// override def initChannel(ch: SocketChannel): Unit = { -// println("initChannel") -//// ch.pipeline().addFirst(new Socks5ProxyHandler(new InetSocketAddress(sys.props("socksProxyHost"), sys.props("socksProxyPort").toInt))) -//// ch.pipeline.addLast(new Nothing) -// } -// }) - -// val isa = new InetSocketAddress("hqa6yepjl2uwzn2y.onion", 9735) -// val ia = isa.getAddress -// println(ia) - -// val f = b.connect("hqa6yepjl2uwzn2y.onion", 9735).sync -//// val f = b.connect("bitcoin.org", 80).sync -// f.channel().closeFuture().sync() -// } finally -// workerGroup.shutdownGracefully() - - - - // val socketChannel = java.nio.channels.SocketChannel.open() -// socketChannel.connect(new InetSocketAddress("hqa6yepjl2uwzn2y.onion", 9735)) - -// val u = new java.net.URL("http://hqa6yepjl2uwzn2y.onion:9735/") -// val c = u.openConnection() -// val r = new java.io.BufferedReader(new java.io.InputStreamReader(c.getInputStream)) -// try { -// r.lines().forEach(new java.util.function.Consumer[String] { -// override def accept(t: String): Unit = { -// println(t) -// } -// }) -// } finally { -// r.close() -// } - -/* + "tor" ignore { + val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(TorProtocolHandler.props( @@ -263,7 +44,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) privateKeyPath = sys.props("user.home") + "/v2_pk", virtualPort = 9999, onionAdded = Some(promiseOnionAddress), - nonce = None), //Some(unhex(ClientNonce))), + nonce = Some(unhex(ClientNonce))), "tor-proto") val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandler), "tor") @@ -273,7 +54,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) println(address.onionService.length) println((address.onionService + ".onion").length) println(NodeAddress(address)) - */ } "happy path v2" in { From 29bbedd734bd188511d91d4d0a40c49d049c18d5 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 4 Nov 2018 21:18:15 -0800 Subject: [PATCH 08/40] some more changes --- TOR.md | 12 +++++++----- eclair-core/src/main/resources/reference.conf | 2 +- .../src/main/scala/fr/acinq/eclair/Setup.scala | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/TOR.md b/TOR.md index 8353db2643..91c6b28b99 100644 --- a/TOR.md +++ b/TOR.md @@ -58,12 +58,14 @@ need to configure Tor hidden service using `eclair.tor.protocol-version` paramet eclair.tor.protocol-version = "v3" ``` +eclair will create a hidden service end point and advertise it's onion address as the node's public address. + There are three possible values for `protocol-version`: -value | description --------|--------------------------------------------------------- - socks | no incoming connections allowed. - v2 | eclair sets up a Tor hidden service version 2 end point - v3 | eclair sets up a Tor hidden service version 3 end point +value | description +--------|--------------------------------------------------------- + socks5 | use SOCKS5 proxy for reaching peers via Tor + v2 | set up a Tor hidden service version 2 end point + v3 | set up a Tor hidden service version 3 end point Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index f319dff0a4..79a5840418 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -83,7 +83,7 @@ eclair { tor { enabled = false - protocol = "v3" // socks, v2, v3 + protocol = "v3" // socks5, v2, v3 host = "127.0.0.1" socks-port = 9050 control-port = 9051 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 4086bd31a8..9b8b233190 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -79,9 +79,6 @@ class Setup(datadir: File, val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, initTor()) - logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") - logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") - // always bind the server to localhost when using Tor val serverBindingAddress = new InetSocketAddress( if (config.getBoolean("tor.enabled")) "127.0.0.1" else config.getString("server.binding-ip"), @@ -92,6 +89,9 @@ class Setup(datadir: File, DBCompatChecker.checkNetworkDBCompatibility(nodeParams) PortChecker.checkAvailable(serverBindingAddress) + logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") + logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") + val bitcoin = nodeParams.watcherType match { case BITCOIND => val bitcoinClient = new BasicBitcoinJsonRPCClient( @@ -286,7 +286,7 @@ class Setup(datadir: File, private def initTor(): Option[InetSocketAddress] = { if (config.getBoolean("tor.enabled")) { - if (config.getString("tor.protocol") != "socks") { + if (config.getString("tor.protocol").toLowerCase != "socks5") { val promiseTorAddress = Promise[OnionAddress]() val protocolHandler = system.actorOf(TorProtocolHandler.props( version = config.getString("tor.protocol"), From ed5d59df8c2428a0688f2bc954e0c2bd85498da8 Mon Sep 17 00:00:00 2001 From: rorp Date: Sat, 17 Nov 2018 20:26:40 -0800 Subject: [PATCH 09/40] stream isolation --- TOR.md | 6 + eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Setup.scala | 6 +- .../scala/fr/acinq/eclair/io/Client.scala | 25 +++-- .../main/scala/fr/acinq/eclair/io/Peer.scala | 5 +- .../fr/acinq/eclair/io/Switchboard.scala | 5 +- .../acinq/eclair/tor/Socks5Connection.scala | 103 ++++++++++++------ 7 files changed, 105 insertions(+), 46 deletions(-) diff --git a/TOR.md b/TOR.md index 91c6b28b99..de06a65715 100644 --- a/TOR.md +++ b/TOR.md @@ -68,4 +68,10 @@ value | description v2 | set up a Tor hidden service version 2 end point v3 | set up a Tor hidden service version 3 end point +To create a new Tor circuit for every connection, use `stream-isolation` parameter: + +``` +eclair.tor.stream-isolation = true +``` + Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 79a5840418..919f49f86d 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -88,5 +88,6 @@ eclair { socks-port = 9050 control-port = 9051 private-key-file = "tor_pk" + stream-isolation = false } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 9b8b233190..403fd00152 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -37,6 +37,7 @@ import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} import fr.acinq.eclair.blockchain.{EclairWallet, _} import fr.acinq.eclair.channel.Register import fr.acinq.eclair.crypto.LocalKeyManager +import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ @@ -216,7 +217,10 @@ class Setup(datadir: File, relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume)) router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher), "router", SupervisorStrategy.Resume)) authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) - socksProxy = if (config.getBoolean("tor.enabled")) Some(new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.socks-port"))) else None + socksProxy = if (config.getBoolean("tor.enabled")) Some(Socks5ProxyParams( + new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.socks-port")), + config.getBoolean("tor.stream-isolation") + )) else None switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index b959dda094..ff75a89d23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -23,10 +23,12 @@ import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.io.Client.ConnectionFailed +import fr.acinq.eclair.io.Client.{ConnectionFailed, Socks5ProxyParams} import fr.acinq.eclair.tor.Socks5Connection import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected} import fr.acinq.eclair.{Logs, NodeParams} +import fr.acinq.eclair.randomBytes +import fr.acinq.bitcoin.toHexString import scala.concurrent.duration._ @@ -34,7 +36,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[InetSocketAddress]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[Socks5ProxyParams]) extends Actor with DiagnosticActorLogging { import Tcp._ import context.system @@ -51,9 +53,9 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke case None => log.info(s"connecting to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort}") address - case Some(socksProxyAddress) => - log.info(s"connecting to SOCKS5 proxy $socksProxyAddress") - socksProxyAddress + case Some(socksProxy) => + log.info(s"connecting to SOCKS5 proxy ${socksProxy.address}") + socksProxy.address } IO(Tcp) ! Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) @@ -80,8 +82,13 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke log.info(s"connected to pubkey=$remoteNodeId host=${remote.getHostString} port=${remote.getPort}") authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) context become connected(connection) - case Some(_) => - connection = context.actorOf(Socks5Connection.props(sender())) + case Some(proxyParams) => + val (username, password) = if (proxyParams.randomizeCredentials) + // randomize credentials for every proxy connection to enable Tor stream isolation + (Some(toHexString(randomBytes(127))), Some(toHexString(randomBytes(127)))) + else + (None, None) + connection = context.actorOf(Socks5Connection.props(sender(), username, password)) context watch connection connection ! Socks5Connect(address) } @@ -108,8 +115,10 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke object Client extends App { - def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[InetSocketAddress]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt, socksProxy)) + def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[Socks5ProxyParams]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt, socksProxy)) case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") + case class Socks5ProxyParams(address: InetSocketAddress, randomizeCredentials: Boolean) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 2d89521721..4935cffbb6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -28,6 +28,7 @@ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} @@ -39,7 +40,7 @@ import scala.util.Random /** * Created by PM on 26/08/2016. */ -class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { +class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { import Peer._ @@ -435,7 +436,7 @@ object Peer { val IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD = 5 minutes - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy)) + def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy)) // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 623a35e8ee..84aca2218d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -23,13 +23,14 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel.HasCommitments +import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.router.Rebroadcast /** * Ties network connections to peers. * Created by PM on 14/02/2017. */ -class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) extends Actor with ActorLogging { +class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) extends Actor with ActorLogging { authenticator ! self @@ -116,6 +117,6 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto object Switchboard { - def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[InetSocketAddress]) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy)) + def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy)) } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index fdeb823a8e..9b9f4b4f31 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -6,48 +6,71 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} import akka.io.Tcp import akka.util.ByteString -class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { +class Socks5Connection(underlying: ActorRef, username: Option[String], password: Option[String]) extends Actor with ActorLogging { + username.foreach(x => require(x.length < 256, "username is too long")) + password.foreach(x => require(x.length < 256, "password is too long")) import fr.acinq.eclair.tor.Socks5Connection._ + private var commander: ActorRef = _ private var handler: ActorRef = _ context watch underlying + def passwordAuth: Boolean = username.isDefined + override def receive: Receive = { case c@Socks5Connect(address) => - context become greetings(sender(), c) + commander = sender() + context become greetings(c) underlying ! Tcp.Register(self) underlying ! Tcp.ResumeReading - underlying ! Tcp.Write(socks5Greeting) + underlying ! Tcp.Write(socks5Greeting(passwordAuth)) } - def greetings(commander: ActorRef, connectCommand: Socks5Connect): Receive = { + def greetings(connectCommand: Socks5Connect): Receive = { case Tcp.Received(data) => - try { + handleExceptions(connectCommand) { if (data(0) != 0x05) { throw new RuntimeException("Invalid SOCKS5 proxy response") - } else if (data(1) != 0x00) { + } else if ((!passwordAuth && data(1) != NoAuth) || (passwordAuth && data(1) != PasswordAuth)) { throw new RuntimeException("Unrecognized SOCKS5 auth method") } else { - context become connectionRequest(commander, connectCommand) - underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) - underlying ! Tcp.ResumeReading + if (data(1) == PasswordAuth) { + context become authenticate(connectCommand) + underlying ! Tcp.Write(socks5PasswordAuthenticationRequest( + username.getOrElse(throw new RuntimeException("username is not defined")), + password.getOrElse(throw new RuntimeException("password is not defined")))) + underlying ! Tcp.ResumeReading + } else { + context become connectionRequest(connectCommand) + underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) + underlying ! Tcp.ResumeReading + } } - } catch { - case e: Throwable => handleErrors("Error connecting to SOCKS5 proxy", commander, connectCommand, e) - } - case c: Tcp.ConnectionClosed => - commander ! c - context stop self + } ("Error connecting to SOCKS5 proxy") + } + + def authenticate(connectCommand: Socks5Connect): Receive = { + case c@Tcp.Received(data) => + handleExceptions(connectCommand) { + if (data(0) != 0x01) { + throw new RuntimeException("Invalid SOCKS5 proxy response") + } else if (data(1) != 0) { + throw new RuntimeException("SOCKS5 authentication failed") + } + context become connectionRequest(connectCommand) + underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) + underlying ! Tcp.ResumeReading + } ("SOCKS5 authentication error") } - def connectionRequest(commander: ActorRef, connectCommand: Socks5Connect): Receive = { + def connectionRequest(connectCommand: Socks5Connect): Receive = { case c@Tcp.Received(data) => - try { - if (data(0) != 0x05) { - throw new RuntimeException("Invalid SOCKS5 proxy response") - } else { + handleExceptions(connectCommand) { + if (data(0) != 0x05) { + throw new RuntimeException("Invalid SOCKS5 proxy response") + } else { val status = data(1) if (status != 0) { throw new RuntimeException(connectErrors.getOrElse(status, s"Unknown SOCKS5 error $status")) @@ -74,13 +97,8 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { context become connected log.info(s"connected $connectedAddress") commander ! Socks5Connected(connectedAddress) - } - } catch { - case e: Throwable => handleErrors("Cannot establish SOCKS5 connection", commander, connectCommand, e) - } - case c: Tcp.ConnectionClosed => - commander ! c - context stop self + } + } ("Cannot establish SOCKS5 connection") } def connected: Receive = { @@ -95,11 +113,19 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { } override def unhandled(message: Any): Unit = message match { - case Terminated(actor) if actor == underlying => context stop self - case _ => log.warning(s"unhandled message=$message") + case Terminated(actor) if actor == underlying => + context stop self + case c: Tcp.ConnectionClosed => + commander ! c + context stop self + case _ => + log.warning(s"unhandled message=$message") } - private def handleErrors(message: String, commander: ActorRef, connectCommand: Socks5Connect, e: Throwable): Unit = { + private def handleExceptions[T](connectCommand: Socks5Connect)(f: => T)(message: => String): Unit = try { + f + } catch { + case e: Throwable => log.error(e, message + " ") underlying ! Tcp.Close commander ! connectCommand.failureMessage @@ -107,12 +133,15 @@ class Socks5Connection(underlying: ActorRef) extends Actor with ActorLogging { } object Socks5Connection { - def props(tcpConnection: ActorRef): Props = Props(new Socks5Connection(tcpConnection)) + def props(tcpConnection: ActorRef, username: Option[String], password: Option[String]): Props = Props(new Socks5Connection(tcpConnection, username, password)) case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event case class Socks5Connect(address: InetSocketAddress) extends Tcp.Command + val NoAuth: Byte = 0x00 + val PasswordAuth: Byte = 0x02 + val connectErrors: Map[Byte, String] = Map[Byte, String]( (0x00, "Request granted"), (0x01, "General failure"), @@ -125,10 +154,18 @@ object Socks5Connection { (0x08, "Address type not supported") ) - val socks5Greeting = ByteString( + def socks5Greeting(passwordAuth: Boolean) = ByteString( 0x05, // SOCKS version 0x01, // number of authentication methods supported - 0x00) // reserved + if (passwordAuth) PasswordAuth else NoAuth) // auth method + + def socks5PasswordAuthenticationRequest(username: String, password: String): ByteString = + ByteString( + 0x01, // version of username/password authentication + username.length.toByte) ++ + ByteString(username) ++ + ByteString(password.length.toByte) ++ + ByteString(password) def socks5ConnectionRequest(address: InetSocketAddress): ByteString = { ByteString( From 4d8f37b0404ba6b359c9c293a59a8e9ee48b09dd Mon Sep 17 00:00:00 2001 From: rorp Date: Wed, 12 Dec 2018 19:20:12 -0800 Subject: [PATCH 10/40] Update TOR.md --- TOR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TOR.md b/TOR.md index de06a65715..9c297dcbe3 100644 --- a/TOR.md +++ b/TOR.md @@ -74,4 +74,4 @@ To create a new Tor circuit for every connection, use `stream-isolation` paramet eclair.tor.stream-isolation = true ``` -Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file +Note, that bitcoind should be configured to use Tor as well. From d5b2e9be551d0fda097afc4c19029a3d5f75c84a Mon Sep 17 00:00:00 2001 From: rorp Date: Sat, 29 Dec 2018 14:19:23 -1000 Subject: [PATCH 11/40] more flexible SOCKS5 configuration --- eclair-core/src/main/resources/reference.conf | 14 +++- .../scala/fr/acinq/eclair/NodeParams.scala | 4 +- .../main/scala/fr/acinq/eclair/Setup.scala | 14 ++-- .../scala/fr/acinq/eclair/io/Client.scala | 64 +++++++++++-------- .../acinq/eclair/tor/TorProtocolHandler.scala | 13 +++- 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1a39eab9ab..ccafbfe27e 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -93,12 +93,20 @@ eclair { max-payment-fee = 0.03 // max total fee for outgoing payments, in percentage: sending a payment will not be attempted if the cheapest route found is more expensive than that min-funding-satoshis = 100000 + socks5 { + enabled = false + host = "127.0.0.1" + port = 9050 + use-for-ipv4 = true + use-for-ipv6 = true + use-for-tor = true + } + tor { enabled = false - protocol = "v3" // socks5, v2, v3 + protocol = "v3" // v2, v3 host = "127.0.0.1" - socks-port = 9050 - control-port = 9051 + port = 9051 private-key-file = "tor_pk" stream-isolation = false } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 1921711a3e..65a3db1adb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -175,7 +175,9 @@ object NodeParams { keyManager = keyManager, alias = config.getString("node-alias").take(32), color = Color(color.data(0), color.data(1), color.data(2)), - publicAddresses = torAddressOpt.map(List(_)).getOrElse(config.getStringList("server.public-ips").toList.map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port")))), + publicAddresses = config.getStringList("server.public-ips").toList + .map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))) + ++ torAddressOpt.map(List(_)).getOrElse(List()), globalFeatures = BinaryData(config.getString("global-features")), localFeatures = BinaryData(config.getString("local-features")), overrideFeatures = overrideFeatures, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index d83a360b32..f7e842a0f6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -83,9 +83,8 @@ class Setup(datadir: File, val nodeParams = NodeParams.makeNodeParams(datadir, config, keyManager, initTor()) - // always bind the server to localhost when using Tor val serverBindingAddress = new InetSocketAddress( - if (config.getBoolean("tor.enabled")) "127.0.0.1" else config.getString("server.binding-ip"), + config.getString("server.binding-ip"), config.getInt("server.port")) // early checks @@ -228,9 +227,12 @@ class Setup(datadir: File, register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume)) relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume)) authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) - socksProxy = if (config.getBoolean("tor.enabled")) Some(Socks5ProxyParams( - new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.socks-port")), - config.getBoolean("tor.stream-isolation") + socksProxy = if (config.getBoolean("socks5.enabled")) Some(Socks5ProxyParams( + new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), + config.getBoolean("tor.stream-isolation"), + config.getBoolean("socks5.use-for-ipv4"), + config.getBoolean("socks5.use-for-ipv6"), + config.getBoolean("socks5.use-for-tor") )) else None switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) @@ -312,7 +314,7 @@ class Setup(datadir: File, "tor-proto") val controller = system.actorOf(Controller.props( - address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.control-port")), + address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")), protocolHandler = protocolHandler), "tor") val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index ff75a89d23..d14fac6294 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress +import java.net.{Inet4Address, Inet6Address, InetSocketAddress} import akka.actor.{Props, _} import akka.event.Logging.MDC @@ -36,7 +36,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[Socks5ProxyParams]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxyParams: Option[Socks5ProxyParams]) extends Actor with DiagnosticActorLogging { import Tcp._ import context.system @@ -49,57 +49,58 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke def receive = { case 'connect => - val addressToConnect = socksProxy match { + val addressToConnect = proxyAddress match { case None => - log.info(s"connecting to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort}") - address - case Some(socksProxy) => - log.info(s"connecting to SOCKS5 proxy ${socksProxy.address}") - socksProxy.address + log.info(s"connecting to pubkey=$remoteNodeId host=${remoteAddress.getHostString} port=${remoteAddress.getPort}") + remoteAddress + case Some(socks5Address) => + log.info(s"connecting to SOCKS5 proxy ${str(socks5Address)}") + socks5Address } IO(Tcp) ! Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) case CommandFailed(_: Connect) => - socksProxy match { + proxyAddress match { case None => - log.info(s"connection failed to $remoteNodeId@${address.getHostString}:${address.getPort}") - case Some(socksProxyAddress) => - log.info(s"connection failed to SOCKS5 proxy $socksProxyAddress") + log.info(s"connection failed to $remoteNodeId@${str(remoteAddress)}") + case Some(socks5Address) => + log.info(s"connection failed to SOCKS5 proxy ${str(socks5Address)}") } - origin_opt.map(_ ! Status.Failure(ConnectionFailed(address))) + origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self case CommandFailed(_: Socks5Connect) => - log.info(s"connection failed to $remoteNodeId@${address.getHostString}:${address.getPort} via SOCKS5 ${socksProxy.map(_.toString).getOrElse("")}") - origin_opt.map(_ ! Status.Failure(ConnectionFailed(address))) + log.info(s"connection failed to $remoteNodeId@${str(remoteAddress)} via SOCKS5 ${proxyAddress.map(str).getOrElse("")}") + origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self case Connected(remote, _) => - socksProxy match { + proxyAddress match { case None => connection = sender() context watch connection log.info(s"connected to pubkey=$remoteNodeId host=${remote.getHostString} port=${remote.getPort}") - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) context become connected(connection) - case Some(proxyParams) => - val (username, password) = if (proxyParams.randomizeCredentials) + case Some(_) => + val (username, password) = if (socksProxyParams.exists(_.randomizeCredentials)) // randomize credentials for every proxy connection to enable Tor stream isolation - (Some(toHexString(randomBytes(127))), Some(toHexString(randomBytes(127)))) + (Some(toHexString(randomBytes(16))), Some(toHexString(randomBytes(16)))) else (None, None) connection = context.actorOf(Socks5Connection.props(sender(), username, password)) context watch connection - connection ! Socks5Connect(address) + connection ! Socks5Connect(remoteAddress) } case Socks5Connected(_) => - socksProxy match { - case Some(proxyAddress) => - log.info(s"connected to pubkey=$remoteNodeId host=${address.getHostString} port=${address.getPort} via SOCKS5 proxy $proxyAddress") - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = address, origin_opt = origin_opt) + proxyAddress match { + case Some(socks5Address) => + log.info(s"connected to pubkey=$remoteNodeId host=${remoteAddress.getHostString} port=${remoteAddress.getPort} via SOCKS5 proxy ${str(socks5Address)}") + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) context become connected(connection) case _ => + log.error("Hmm.") } } @@ -111,6 +112,17 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke override def unhandled(message: Any): Unit = log.warning(s"unhandled message=$message") override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) + + private def proxyAddress: Option[InetSocketAddress] = socksProxyParams.flatMap { proxyParams => + remoteAddress.getAddress match { + case _ if remoteAddress.getHostString.endsWith(".onion") => if (proxyParams.useForTor) Some(proxyParams.address) else None + case _: Inet4Address => if (proxyParams.useForIPv4) Some(proxyParams.address) else None + case _: Inet6Address =>if (proxyParams.useForIPv6) Some(proxyParams.address) else None + case _ => None + } + } + + private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}" } object Client extends App { @@ -119,6 +131,6 @@ object Client extends App { case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") - case class Socks5ProxyParams(address: InetSocketAddress, randomizeCredentials: Boolean) + case class Socks5ProxyParams(address: InetSocketAddress, randomizeCredentials: Boolean, useForIPv4: Boolean, useForIPv6: Boolean, useForTor: Boolean) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 085cf6e0a8..ac14e8e630 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -1,7 +1,8 @@ package fr.acinq.eclair.tor import java.io._ -import java.nio.file.{Files, Paths} +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{FileSystems, Files, Paths} import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash, Status} import akka.io.Tcp.Connected @@ -110,7 +111,10 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, private def processOnionResponse(res: Map[String, String]): String = { val serviceId = res.getOrElse("ServiceID", throw TorException("Tor service ID not found")) val privateKey = res.get("PrivateKey") - privateKey.foreach(writeString(privateKeyPath, _)) + privateKey.foreach { pk => + writeString(privateKeyPath, pk) + setPersissions(privateKeyPath, "rw-------") + } serviceId } @@ -235,6 +239,11 @@ object TorProtocolHandler { } } + def setPersissions(filename: String, permissionString: String): Unit = { + val path = FileSystems.getDefault.getPath(filename) + Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString)) + } + def unquote(s: String): String = s .stripSuffix("\"") .stripPrefix("\"") From 5bda47c6705ee2515d8f9503d8d194b46f1f4dad Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 6 Jan 2019 12:19:30 -0800 Subject: [PATCH 12/40] more flexible config --- TOR.md | 56 +++++++++++++------ .../main/scala/fr/acinq/eclair/Setup.scala | 45 +++++++-------- .../scala/fr/acinq/eclair/api/Service.scala | 2 +- .../fr/acinq/eclair/tor/Controller.scala | 22 +++++--- .../fr/acinq/eclair/tor/OnionAddress.scala | 3 + .../acinq/eclair/tor/Socks5Connection.scala | 19 +++++-- .../acinq/eclair/tor/TorProtocolHandler.scala | 10 ++++ 7 files changed, 102 insertions(+), 55 deletions(-) diff --git a/TOR.md b/TOR.md index de06a65715..a8cdf3c28b 100644 --- a/TOR.md +++ b/TOR.md @@ -14,9 +14,9 @@ For Mac OS X: brew install tor ``` -Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X) -eclair requires for safe cooke authentication as well as SOCKS5 and control connections to be enabled. -Change value of `ExitPolicy` parameter only if you really know what you are doing. +Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X). +Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled. +Change the value of the `ExitPolicy` parameter only if you really know what you are doing. ``` @@ -26,7 +26,7 @@ CookieAuthentication 1 ExitPolicy reject *:* ``` -Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`) +Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`). ### Start Tor @@ -42,31 +42,34 @@ For Mac OS X: brew services start tor ``` -### Configure eclair to use Tor - -To enable Tor support simply set `eclair.tor.enabled` parameter in `eclair.conf` to true. +### Configure Tor hidden service +To create a Tor hidden service endpoint simply set the `eclair.tor.enabled` parameter in `eclair.conf` to true. ``` eclair.tor.enabled = true ``` +Eclair will automatically set up a hidden service endpoint and add its onion address to the `server.public-ips` list. +You can see what onion address is assigned using `eclair-cli`: + +```shell +eclair-cli getinfo +``` +Eclair saves the Tor endpoint's private key in `~/.eclair/tor_pk`, so that it can recreate the endpoint address after +restart. If you remove the private key eclair will regenerate the endpoint address. -By default all traffic will be forwarded through Tor network. Note that in this case the value of `eclair.server.public-ip` -will be ignored and incoming connections will be disabled. To enable incoming connections you -need to configure Tor hidden service using `eclair.tor.protocol-version` parameter. +There are two possible values for `protocol-version`: ``` eclair.tor.protocol-version = "v3" ``` -eclair will create a hidden service end point and advertise it's onion address as the node's public address. - -There are three possible values for `protocol-version`: - value | description --------|--------------------------------------------------------- - socks5 | use SOCKS5 proxy for reaching peers via Tor v2 | set up a Tor hidden service version 2 end point - v3 | set up a Tor hidden service version 3 end point + v3 | set up a Tor hidden service version 3 end point (default) + +Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports +both v2 and v3 addresses. To create a new Tor circuit for every connection, use `stream-isolation` parameter: @@ -74,4 +77,23 @@ To create a new Tor circuit for every connection, use `stream-isolation` paramet eclair.tor.stream-isolation = true ``` -Note, that bitcoind should be configured to use Tor as well. \ No newline at end of file +For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`: +``` +eclair.server.binding-ip = "127.0.0.1" +``` + +### Configure SOCKS5 proxy + +By default all incoming connections will be established via Tor network, but all outgoing will be created via the +clearnet. To route them through Tor you can use Tor's SOCKS5 proxy. Add this line in your `eclair.conf`: +``` +eclair.socks5.enabled = true +``` +You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6` +or `eclair.socks5.use-for-tor` for fine tuning. + +--- +Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get the most privacy +features from using Tor use both. + +Note, that bitcoind should be configured to use Tor as well (https://en.bitcoin.it/wiki/Setting_up_a_Tor_hidden_service). \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index f7e842a0f6..fc3ecffc84 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -56,9 +56,9 @@ import scala.concurrent.duration._ * * Created by PM on 25/01/2016. * - * @param datadir directory where eclair-core will write/read its data. + * @param datadir directory where eclair-core will write/read its data. * @param overrideDefaults use this parameter to programmatically override the node configuration . - * @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file. + * @param seed_opt optional seed, if set eclair will use it instead of generating one and won't create a seed.dat file. */ class Setup(datadir: File, overrideDefaults: Config = ConfigFactory.empty(), @@ -179,7 +179,7 @@ class Setup(datadir: File, feeProvider = (nodeParams.chainHash, bitcoin) match { case (Block.RegtestGenesisBlock.hash, _) => new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) case (_, Bitcoind(bitcoinClient)) => - new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! + new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! case _ => new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(), smoothFeerateWindow) :: new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) // order matters! } @@ -275,7 +275,8 @@ class Setup(datadir: File, alias = nodeParams.alias, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, - blockHeight = Globals.blockCount.intValue())) + blockHeight = Globals.blockCount.intValue(), + publicAddresses = nodeParams.publicAddresses.map(_.getHostString))) override def appKit: Kit = kit @@ -303,26 +304,22 @@ class Setup(datadir: File, private def initTor(): Option[InetSocketAddress] = { if (config.getBoolean("tor.enabled")) { - if (config.getString("tor.protocol").toLowerCase != "socks5") { - val promiseTorAddress = Promise[OnionAddress]() - val protocolHandler = system.actorOf(TorProtocolHandler.props( - version = config.getString("tor.protocol"), - privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, - virtualPort = config.getInt("server.port"), - onionAdded = Some(promiseTorAddress), - nonce = None), - "tor-proto") - - val controller = system.actorOf(Controller.props( - address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")), - protocolHandler = protocolHandler), "tor") - - val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") - logger.info(s"Tor address ${torAddress.toOnion}") - Some(torAddress.toInetSocketAddress) - } else { - None - } + val promiseTorAddress = Promise[OnionAddress]() + val protocolHandler = system.actorOf(TorProtocolHandler.props( + version = config.getString("tor.protocol"), + privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, + virtualPort = config.getInt("server.port"), + onionAdded = Some(promiseTorAddress), + nonce = None), + "tor-proto") + + val controller = system.actorOf(Controller.props( + address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")), + protocolHandler = protocolHandler), "tor") + + val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") + logger.info(s"Tor address ${torAddress.toOnion}") + Some(torAddress.toInetSocketAddress) } else { None } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index ea5d8de5ee..ec652d603d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -56,7 +56,7 @@ case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", meth case class Error(code: Int, message: String) case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String) case class Status(node_id: String) -case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int) +case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int, publicAddresses: Seq[String]) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) trait RPCRejection extends Rejection { def requestId: String diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala index 661aa0385b..3d3b2ad0ad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala @@ -8,7 +8,14 @@ import akka.util.ByteString import scala.concurrent.ExecutionContext -class Controller(address: InetSocketAddress, listener: ActorRef) +/** + * Created by rorp + * + * @param address Tor control address + * @param protocolHandler Tor protocol handler + * @param ec execution context + */ +class Controller(address: InetSocketAddress, protocolHandler: ActorRef) (implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { import Controller._ @@ -23,10 +30,10 @@ class Controller(address: InetSocketAddress, listener: ActorRef) case Some(ex) => log.error(ex, "Cannot connect") case _ => log.error("Cannot connect") } - context stop listener + context stop protocolHandler context stop self case c@Connected(remote, local) => - listener ! c + protocolHandler ! c val connection = sender() connection ! Register(self) context watch connection @@ -35,15 +42,15 @@ class Controller(address: InetSocketAddress, listener: ActorRef) connection ! Write(data) case CommandFailed(w: Write) => // O/S buffer was full - listener ! SendFailed + protocolHandler ! SendFailed log.error("Tor command failed") case Received(data) => - listener ! data + protocolHandler ! data case _: ConnectionClosed => - context stop listener + context stop protocolHandler context stop self case Terminated(actor) if actor == connection => - context stop listener + context stop protocolHandler context stop self } } @@ -55,4 +62,5 @@ object Controller { Props(new Controller(address, protocolHandler)) case object SendFailed + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala index 5fc02ed62a..85bc570c06 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala @@ -4,6 +4,9 @@ import java.net.InetSocketAddress import org.apache.commons.codec.binary.Base32 +/** + * Created by rorp + */ sealed trait OnionAddress { import OnionAddress._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 9b9f4b4f31..595957a86e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -6,6 +6,13 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} import akka.io.Tcp import akka.util.ByteString +/** + * Created by rorp + * + * @param underlying underlying TcpConnection + * @param username user name for password authentication + * @param password password for password authentication + */ class Socks5Connection(underlying: ActorRef, username: Option[String], password: Option[String]) extends Actor with ActorLogging { username.foreach(x => require(x.length < 256, "username is too long")) password.foreach(x => require(x.length < 256, "password is too long")) @@ -48,7 +55,7 @@ class Socks5Connection(underlying: ActorRef, username: Option[String], password: underlying ! Tcp.ResumeReading } } - } ("Error connecting to SOCKS5 proxy") + }("Error connecting to SOCKS5 proxy") } def authenticate(connectCommand: Socks5Connect): Receive = { @@ -62,7 +69,7 @@ class Socks5Connection(underlying: ActorRef, username: Option[String], password: context become connectionRequest(connectCommand) underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) underlying ! Tcp.ResumeReading - } ("SOCKS5 authentication error") + }("SOCKS5 authentication error") } def connectionRequest(connectCommand: Socks5Connect): Receive = { @@ -98,7 +105,7 @@ class Socks5Connection(underlying: ActorRef, username: Option[String], password: log.info(s"connected $connectedAddress") commander ! Socks5Connected(connectedAddress) } - } ("Cannot establish SOCKS5 connection") + }("Cannot establish SOCKS5 connection") } def connected: Receive = { @@ -126,9 +133,9 @@ class Socks5Connection(underlying: ActorRef, username: Option[String], password: f } catch { case e: Throwable => - log.error(e, message + " ") - underlying ! Tcp.Close - commander ! connectCommand.failureMessage + log.error(e, message + " ") + underlying ! Tcp.Close + commander ! connectCommand.failureMessage } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index ac14e8e630..8a83275487 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -17,6 +17,16 @@ import scala.concurrent.Promise case class TorException(msg: String) extends RuntimeException(msg) +/** + * Created by rorp + * + * @param protocolVersion Tor protocol version + * @param privateKeyPath path to a file that contains a Tor private key + * @param virtualPort Tor virtual port + * @param targetPorts target ports + * @param onionAdded a Promise to track creation of the endpoint + * @param clientNonce optional client nonce, will be randomly generated if omitted + */ class TorProtocolHandler(protocolVersion: ProtocolVersion, privateKeyPath: String, virtualPort: Int, From 7c96f0bbf229271317a7f2c326f014856ca2e87c Mon Sep 17 00:00:00 2001 From: rorp Date: Fri, 11 Jan 2019 11:59:55 -0800 Subject: [PATCH 13/40] fix unit tests --- eclair-core/src/test/resources/api/getinfo | 1 + .../test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index 18c706b723..b30d616859 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1,5 +1,6 @@ { "result" : { + "publicAddresses" : [ "localhost" ], "alias" : "alice", "port" : 9735, "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala index 5761095702..fccad89dfc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala @@ -195,7 +195,8 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { alias = Alice.nodeParams.alias, port = 9735, chainHash = Alice.nodeParams.chainHash, - blockHeight = 123456 + blockHeight = 123456, + publicAddresses = Alice.nodeParams.publicAddresses.map(_.getHostString) )) } import mockService.formats From ced223c0d54fa46af7323adf4ed1613e97070105 Mon Sep 17 00:00:00 2001 From: rorp Date: Sun, 27 Jan 2019 21:20:55 -0800 Subject: [PATCH 14/40] fix build --- eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index 3523d684b4..cb0d7c9d8d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -34,7 +34,7 @@ class StartupSpec extends FunSuite { val keyManager = new LocalKeyManager(seed = randomKey.toBin, chainHash = Block.TestnetGenesisBlock.hash) // try to create a NodeParams instance with a conf that contains an illegal alias - val nodeParamsAttempt = Try(NodeParams.makeNodeParams(tempConfParentDir, conf, keyManager)) + val nodeParamsAttempt = Try(NodeParams.makeNodeParams(tempConfParentDir, conf, keyManager, None)) assert(nodeParamsAttempt.isFailure && nodeParamsAttempt.failed.get.getMessage.contains("alias, too long")) // destroy conf files after the test From cd7ae5dcec5f2c10cd8c5df9e8307679279c3236 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 13:15:43 +0100 Subject: [PATCH 15/40] better doc for virtualPort/targetPorts --- .../main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 8a83275487..bd54e35eb8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -22,8 +22,8 @@ case class TorException(msg: String) extends RuntimeException(msg) * * @param protocolVersion Tor protocol version * @param privateKeyPath path to a file that contains a Tor private key - * @param virtualPort Tor virtual port - * @param targetPorts target ports + * @param virtualPort port of our protected local server (typically 9735) + * @param targetPorts target ports of the public hidden service * @param onionAdded a Promise to track creation of the endpoint * @param clientNonce optional client nonce, will be randomly generated if omitted */ From 8893c1ea53f040adf45cc8455cb1faf1379a3e05 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 14:36:33 +0100 Subject: [PATCH 16/40] fixed typo in setPermissions --- .../main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index bd54e35eb8..f09421dc9a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -123,7 +123,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, val privateKey = res.get("PrivateKey") privateKey.foreach { pk => writeString(privateKeyPath, pk) - setPersissions(privateKeyPath, "rw-------") + setPermissions(privateKeyPath, "rw-------") } serviceId } @@ -249,7 +249,7 @@ object TorProtocolHandler { } } - def setPersissions(filename: String, permissionString: String): Unit = { + def setPermissions(filename: String, permissionString: String): Unit = { val path = FileSystems.getDefault.getPath(filename) Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString)) } From a3d5e48636a2f2fd5b0a74bea687b2a93f751376 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 16:09:38 +0100 Subject: [PATCH 17/40] added link to the tor control protocol spec --- .../src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index f09421dc9a..f1d1889811 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -20,6 +20,8 @@ case class TorException(msg: String) extends RuntimeException(msg) /** * Created by rorp * + * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt + * * @param protocolVersion Tor protocol version * @param privateKeyPath path to a file that contains a Tor private key * @param virtualPort port of our protected local server (typically 9735) From 6a95131da0006efe8659c89e4c44917fb0532631 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 16:10:21 +0100 Subject: [PATCH 18/40] ignore set Permissions errors on windows --- .../main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index f1d1889811..d22c5501b5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -253,7 +253,11 @@ object TorProtocolHandler { def setPermissions(filename: String, permissionString: String): Unit = { val path = FileSystems.getDefault.getPath(filename) - Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString)) + try { + Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString)) + } catch { + case _: UnsupportedOperationException => () // we are on windows + } } def unquote(s: String): String = s From 0a3329a4bc0f54fece73273d56198b13b7550f04 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 16:11:41 +0100 Subject: [PATCH 19/40] proper error management Use actor supervision hierarchy instead of manual error management. An error happening in the TorProtocolHandler actor should also kill the Controller. Also, use a fast-fail so that we have an immediate error in case something goes wrong. --- .../main/scala/fr/acinq/eclair/Setup.scala | 9 +++--- .../fr/acinq/eclair/tor/Controller.scala | 25 ++++++++-------- .../acinq/eclair/tor/TorProtocolHandler.scala | 30 ++++++++----------- .../eclair/tor/TorProtocolHandlerSpec.scala | 7 ++--- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index e82c5229db..9e25a1269f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -313,17 +313,16 @@ class Setup(datadir: File, private def initTor(): Option[InetSocketAddress] = { if (config.getBoolean("tor.enabled")) { val promiseTorAddress = Promise[OnionAddress]() - val protocolHandler = system.actorOf(TorProtocolHandler.props( + val protocolHandlerProps = TorProtocolHandler.props( version = config.getString("tor.protocol"), privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, virtualPort = config.getInt("server.port"), onionAdded = Some(promiseTorAddress), - nonce = None), - "tor-proto") + nonce = None) - val controller = system.actorOf(Controller.props( + val controller = system.actorOf(SimpleSupervisor.props(Controller.props( address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")), - protocolHandler = protocolHandler), "tor") + protocolHandlerProps = protocolHandlerProps), "tor", SupervisorStrategy.Stop)) val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") logger.info(s"Tor address ${torAddress.toOnion}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala index 3d3b2ad0ad..b0cf6c9f0b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala @@ -2,20 +2,20 @@ package fr.acinq.eclair.tor import java.net.InetSocketAddress -import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} +import akka.actor.{Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy, Terminated} import akka.io.{IO, Tcp} import akka.util.ByteString -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Promise} /** * Created by rorp * - * @param address Tor control address - * @param protocolHandler Tor protocol handler - * @param ec execution context + * @param address Tor control address + * @param protocolHandlerProps Tor protocol handler props + * @param ec execution context */ -class Controller(address: InetSocketAddress, protocolHandler: ActorRef) +class Controller(address: InetSocketAddress, protocolHandlerProps: Props) (implicit ec: ExecutionContext = ExecutionContext.global) extends Actor with ActorLogging { import Controller._ @@ -30,9 +30,9 @@ class Controller(address: InetSocketAddress, protocolHandler: ActorRef) case Some(ex) => log.error(ex, "Cannot connect") case _ => log.error("Cannot connect") } - context stop protocolHandler context stop self - case c@Connected(remote, local) => + case c: Connected => + val protocolHandler = context actorOf protocolHandlerProps protocolHandler ! c val connection = sender() connection ! Register(self) @@ -47,19 +47,20 @@ class Controller(address: InetSocketAddress, protocolHandler: ActorRef) case Received(data) => protocolHandler ! data case _: ConnectionClosed => - context stop protocolHandler context stop self case Terminated(actor) if actor == connection => - context stop protocolHandler context stop self } } + // we should not restart a failing tor session + override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Escalate } + } object Controller { - def props(address: InetSocketAddress, protocolHandler: ActorRef)(implicit ec: ExecutionContext = ExecutionContext.global) = - Props(new Controller(address, protocolHandler)) + def props(address: InetSocketAddress, protocolHandlerProps: Props)(implicit ec: ExecutionContext = ExecutionContext.global) = + Props(new Controller(address, protocolHandlerProps)) case object SendFailed diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index d22c5501b5..c45a2df8c2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -4,7 +4,7 @@ import java.io._ import java.nio.file.attribute.PosixFilePermissions import java.nio.file.{FileSystems, Files, Paths} -import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash, Status} +import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString import fr.acinq.eclair.randomBytes @@ -39,6 +39,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, import TorProtocolHandler._ + // those are defined in the spec private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() @@ -56,7 +57,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, } def protocolInfo: Receive = { - case data: ByteString => handleExceptions { + case data: ByteString => val res = parseResponse(readResponse(data)) val protoInfo = ProtocolInfo( methods = res.getOrElse("METHODS", throw TorException("Tor auth methods not found")), @@ -68,11 +69,10 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, } sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") context become authChallenge(protoInfo.cookieFile) - } } def authChallenge(cookieFile: String): Receive = { - case data: ByteString => handleExceptions { + case data: ByteString => val res = parseResponse(readResponse(data)) val clientHash = computeClientHash( res.getOrElse("SERVERHASH", throw TorException("Tor server hash not found")), @@ -81,19 +81,18 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, ) sendCommand(s"AUTHENTICATE ${hex(clientHash)}") context become authenticate - } } + def authenticate: Receive = { - case data: ByteString => handleExceptions { + case data: ByteString => readResponse(data) sendCommand(s"ADD_ONION $computeKey $computePort") context become addOnion - } } def addOnion: Receive = { - case data: ByteString => handleExceptions { + case data: ByteString => val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) @@ -104,7 +103,6 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, onionAdded.foreach(_.success(address.get)) log.debug(s"Onion address: ${address.get}") } - } } override def unhandled(message: Any): Unit = message match { @@ -112,14 +110,6 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, sender() ! address } - private def handleExceptions[T](f: => T): Unit = try { - f - } catch { - case e: Exception => - log.error(e, "Tor error: ") - sender ! Status.Failure(e) - } - private def processOnionResponse(res: Map[String, String]): String = { val serviceId = res.getOrElse("ServiceID", throw TorException("Tor service ID not found")) val privateKey = res.get("PrivateKey") @@ -173,6 +163,11 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, private def sendCommand(cmd: String): Unit = { receiver ! ByteString(s"$cmd\r\n") } + + override def postStop(): Unit = { + super.postStop() + onionAdded.foreach(_.tryFailure(new RuntimeException("tor connection exception"))) + } } object TorProtocolHandler { @@ -232,7 +227,6 @@ object TorProtocolHandler { } } - def readString(filename: String): String = { val r = new BufferedReader(new FileReader(filename)) try { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index d18f0cd37e..9ae5beb6ea 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -39,15 +39,14 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val promiseOnionAddress = Promise[OnionAddress]() - val protocolHandler = TestActorRef(TorProtocolHandler.props( + val protocolHandlerProps = TorProtocolHandler.props( version ="v2", privateKeyPath = sys.props("user.home") + "/v2_pk", virtualPort = 9999, onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), - "tor-proto") + nonce = Some(unhex(ClientNonce))) - val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandler), "tor") + val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandlerProps), "tor") val address = Await.result(promiseOnionAddress.future, 30 seconds) println(address) From fa3c6a7200a08493c27b9a3bc1baab8faa4f39db Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 17:09:19 +0100 Subject: [PATCH 20/40] added windows instructions to TOR.md --- TOR.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/TOR.md b/TOR.md index 29404c4206..d286bdd7ab 100644 --- a/TOR.md +++ b/TOR.md @@ -14,10 +14,14 @@ For Mac OS X: brew install tor ``` -Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X). -Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled. -Change the value of the `ExitPolicy` parameter only if you really know what you are doing. +For Windows: + +Download the "Expert Bundle" from https://www.torproject.org/download/download.html and extract it to the root of your drive (e.g. `C:\tor-win32-0.3.5.7`). +Edit Tor configuration file: + - `/etc/tor/torrc` (Linux) + - `/usr/local/etc/tor/torrc` (Mac OS X) + - `C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\tor\torrc` (Windows) ``` SOCKSPort 9050 @@ -26,6 +30,9 @@ CookieAuthentication 1 ExitPolicy reject *:* ``` +Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled. +Change the value of the `ExitPolicy` parameter only if you really know what you are doing. + Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`). ### Start Tor @@ -42,6 +49,14 @@ For Mac OS X: brew services start tor ``` +For Windows: + +Open a CMD with administrator access + +```shell +tor --service install +``` + ### Configure Tor hidden service To create a Tor hidden service endpoint simply set the `eclair.tor.enabled` parameter in `eclair.conf` to true. @@ -92,7 +107,7 @@ eclair.socks5.enabled = true You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6` or `eclair.socks5.use-for-tor` for fine tuning. -Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get the most privacy +:warning: Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get the most privacy features from using Tor use both. Note, that bitcoind should be configured to use Tor as well (https://en.bitcoin.it/wiki/Setting_up_a_Tor_hidden_service). \ No newline at end of file From 3cf82a6f360959388dbd95879dd4bfb92d011811 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 17:10:15 +0100 Subject: [PATCH 21/40] renamed file tor_pk->tor.dat for consistency --- eclair-core/src/main/resources/reference.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index d3f8113d65..68d385739b 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -111,7 +111,7 @@ eclair { protocol = "v3" // v2, v3 host = "127.0.0.1" port = 9051 - private-key-file = "tor_pk" + private-key-file = "tor.dat" stream-isolation = false } } \ No newline at end of file From c57eba118700e8015b907682be769406c8191d32 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 31 Jan 2019 17:23:58 +0100 Subject: [PATCH 22/40] moved constants to companion object --- .../scala/fr/acinq/eclair/tor/TorProtocolHandler.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index c45a2df8c2..e5c230c1ad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -39,10 +39,6 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, import TorProtocolHandler._ - // those are defined in the spec - private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() - private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() - private var receiver: ActorRef = _ private var address: Option[OnionAddress] = None @@ -180,6 +176,10 @@ object TorProtocolHandler { ): Props = Props(new TorProtocolHandler(ProtocolVersion(version), privateKeyPath, virtualPort, targetPorts, onionAdded, nonce)) + // those are defined in the spec + private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() + private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() + val MinV3Version = "0.3.3.6" sealed trait ProtocolVersion { From bcc3d53690fd7b71f603fd58ae5cc22d1bbf4bcc Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 1 Feb 2019 15:15:52 +0100 Subject: [PATCH 23/40] only support v3 hidden service Eclair can connect to v2/v3, but will only open a v3 service. Instead of trying our best to be compatible, we just require a minimal version for the tor daemon. The rationale for this change is that it allows for a significant simplification, and is recommended in the Tor documentation: > Since Tor 0.3.2 and Tor Browser 7.5.a5 56-character long v3 onion addresses are supported and should be used instead. source: https://www.torproject.org/docs/tor-onion-service.html.en#four --- TOR.md | 16 +- eclair-core/src/main/resources/reference.conf | 1 - .../main/scala/fr/acinq/eclair/Setup.scala | 1 - .../acinq/eclair/tor/TorProtocolHandler.scala | 55 +---- .../scala/fr/acinq/eclair/tor/Version.scala | 26 --- .../eclair/tor/TorProtocolHandlerSpec.scala | 217 +----------------- 6 files changed, 13 insertions(+), 303 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala diff --git a/TOR.md b/TOR.md index d286bdd7ab..4d04a283b7 100644 --- a/TOR.md +++ b/TOR.md @@ -2,6 +2,8 @@ ### Installing Tor on your node +Eclair requires Tor 0.3.3.6+. + For Linux: ```shell @@ -72,20 +74,6 @@ eclair-cli getinfo Eclair saves the Tor endpoint's private key in `~/.eclair/tor_pk`, so that it can recreate the endpoint address after restart. If you remove the private key eclair will regenerate the endpoint address. -There are two possible values for `protocol-version`: - -``` -eclair.tor.protocol-version = "v3" -``` - -value | description ---------|--------------------------------------------------------- - v2 | set up a Tor hidden service version 2 end point - v3 | set up a Tor hidden service version 3 end point (default) - -Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports -both v2 and v3 addresses. - To create a new Tor circuit for every connection, use `stream-isolation` parameter: ``` diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 68d385739b..28bfca0898 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -108,7 +108,6 @@ eclair { tor { enabled = false - protocol = "v3" // v2, v3 host = "127.0.0.1" port = 9051 private-key-file = "tor.dat" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 9e25a1269f..dd0b8ade07 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -314,7 +314,6 @@ class Setup(datadir: File, if (config.getBoolean("tor.enabled")) { val promiseTorAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( - version = config.getString("tor.protocol"), privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, virtualPort = config.getInt("server.port"), onionAdded = Some(promiseTorAddress), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index e5c230c1ad..9221119767 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -8,7 +8,6 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString import fr.acinq.eclair.randomBytes -import fr.acinq.eclair.tor.TorProtocolHandler.ProtocolVersion import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import javax.xml.bind.DatatypeConverter @@ -22,15 +21,13 @@ case class TorException(msg: String) extends RuntimeException(msg) * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * - * @param protocolVersion Tor protocol version - * @param privateKeyPath path to a file that contains a Tor private key - * @param virtualPort port of our protected local server (typically 9735) - * @param targetPorts target ports of the public hidden service - * @param onionAdded a Promise to track creation of the endpoint - * @param clientNonce optional client nonce, will be randomly generated if omitted + * @param privateKeyPath path to a file that contains a Tor private key + * @param virtualPort port of our protected local server (typically 9735) + * @param targetPorts target ports of the public hidden service + * @param onionAdded a Promise to track creation of the endpoint + * @param clientNonce optional client nonce, will be randomly generated if omitted */ -class TorProtocolHandler(protocolVersion: ProtocolVersion, - privateKeyPath: String, +class TorProtocolHandler(privateKeyPath: String, virtualPort: Int, targetPorts: Seq[Int], onionAdded: Option[Promise[OnionAddress]], @@ -60,9 +57,6 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, cookieFile = unquote(res.getOrElse("COOKIEFILE", throw TorException("Tor cookie file not found"))), version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found")))) log.info(s"Tor version ${protoInfo.version}") - if (!protocolVersion.supportedBy(protoInfo.version)) { - throw TorException(s"Tor version ${protoInfo.version} does not support protocol $protocolVersion") - } sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") context become authChallenge(protoInfo.cookieFile) } @@ -92,10 +86,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) - address = Some(protocolVersion match { - case V2 => OnionAddressV2(serviceId, virtualPort) - case V3 => OnionAddressV3(serviceId, virtualPort) - }) + address = Some(OnionAddressV3(serviceId, virtualPort)) onionAdded.foreach(_.success(address.get)) log.debug(s"Onion address: ${address.get}") } @@ -120,10 +111,7 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, if (Files.exists(Paths.get(privateKeyPath))) { readString(privateKeyPath) } else { - protocolVersion match { - case V2 => "NEW:RSA1024" - case V3 => "NEW:ED25519-V3" - } + "NEW:ED25519-V3" } } @@ -167,41 +155,18 @@ class TorProtocolHandler(protocolVersion: ProtocolVersion, } object TorProtocolHandler { - def props(version: String, - privateKeyPath: String, + def props(privateKeyPath: String, virtualPort: Int, targetPorts: Seq[Int] = Seq(), onionAdded: Option[Promise[OnionAddress]] = None, nonce: Option[Array[Byte]] = None ): Props = - Props(new TorProtocolHandler(ProtocolVersion(version), privateKeyPath, virtualPort, targetPorts, onionAdded, nonce)) + Props(new TorProtocolHandler(privateKeyPath, virtualPort, targetPorts, onionAdded, nonce)) // those are defined in the spec private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() - val MinV3Version = "0.3.3.6" - - sealed trait ProtocolVersion { - def supportedBy(torVersion: String): Boolean - } - - case object V2 extends ProtocolVersion { - override def supportedBy(torVersion: String): Boolean = true - } - - case object V3 extends ProtocolVersion { - override def supportedBy(torVersion: String): Boolean = Version(torVersion) >= Version(MinV3Version) - } - - object ProtocolVersion { - def apply(s: String): ProtocolVersion = s match { - case "v2" | "V2" => V2 - case "v3" | "V3" => V3 - case _ => throw TorException(s"Unknown protocol version `$s`") - } - } - case object GetOnionAddress case class ProtocolInfo(methods: String, cookieFile: String, version: String) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala deleted file mode 100644 index 7a6bede1eb..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Version.scala +++ /dev/null @@ -1,26 +0,0 @@ -package fr.acinq.eclair.tor - -import scala.util.Try - -case class Version(value: String) extends Ordered[Version] { - lazy val parts: Seq[String] = { - val ps = value.split('.') - // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) - ps.update(ps.length - 1, ps.last.split('-').head) - ps - } - - override def compare(that: Version): Int = { - parts - .zipAll(that.parts, "", "") - .find { case (a, b) => a != b } - .map { case (a, b) => - (for { - x <- Try(a.toInt) - y <- Try(b.toInt) - } yield x.compare(y)) - .getOrElse(a.compare(b)) - } - .getOrElse(0) - } -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index 9ae5beb6ea..bec47b0c99 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -1,11 +1,9 @@ package fr.acinq.eclair.tor import java.io.{File, IOException} -import java.net.{InetAddress, InetSocketAddress, Socket} -import java.nio.charset.Charset +import java.net.InetSocketAddress import java.nio.file.attribute.BasicFileAttributes import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} -import java.util.Date import akka.actor.ActorSystem import akka.actor.Status.Failure @@ -40,7 +38,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val promiseOnionAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( - version ="v2", privateKeyPath = sys.props("user.home") + "/v2_pk", virtualPort = 9999, onionAdded = Some(promiseOnionAddress), @@ -55,61 +52,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) println(NodeAddress(address)) } - "happy path v2" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie)) - - val promiseOnionAddress = Promise[OnionAddress]() - - val protocolHandler = TestActorRef(props( - version = "v2", - privateKeyPath = pkFile, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "happy-v2") - - protocolHandler ! Connected(LocalHost, LocalHost) - - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) - - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) - protocolHandler ! ByteString( - "250 OK\r\n" - ) - - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) - protocolHandler ! ByteString( - "250-ServiceID=z4zif3fy7fe7bpg3\r\n" + - "250-PrivateKey=RSA1024:private-key\r\n" + - "250 OK\r\n" - ) - - protocolHandler ! GetOnionAddress - expectMsg(Some(OnionAddressV2("z4zif3fy7fe7bpg3", 9999))) - - val address = Await.result(promiseOnionAddress.future, 3 seconds) - address must be(OnionAddressV2("z4zif3fy7fe7bpg3", 9999)) - address.toOnion must be ("z4zif3fy7fe7bpg3.onion:9999") - NodeAddress(address).toString must be ("Tor2(cf3282ecb8f949f0bcdb,9999)") - - readString(pkFile) must be("RSA1024:private-key") - } - } - "happy path v3" in { withTempDir { dir => val cookieFile = dir + File.separator + "cookie" @@ -120,7 +62,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(props( - version = "v3", privateKeyPath = pkFile, virtualPort = 9999, onionAdded = Some(promiseOnionAddress), @@ -167,7 +108,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) "v3 should not be supported by 0.3.3.5" in { val protocolHandler = TestActorRef(props( - version = "v3", privateKeyPath = "", virtualPort = 9999, onionAdded = None, @@ -189,7 +129,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) "v3 should handle AUTHCHALLENGE errors" in { val protocolHandler = TestActorRef(props( - version = "v3", privateKeyPath = "", virtualPort = 9999, onionAdded = None, @@ -213,160 +152,6 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) expectMsg(Failure(TorException("Tor server returned error: 513 Invalid base16 client nonce"))) } - "v2 should handle invalid cookie file" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie).take(2)) - - val promiseOnionAddress = Promise[OnionAddress]() - - val protocolHandler = TestActorRef(props( - version = "v2", - privateKeyPath = pkFile, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "invalid-cookie") - - protocolHandler ! Connected(LocalHost, LocalHost) - - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) - - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(Failure(TorException("Invalid file length"))) - } - } - - "v2 should handle server hash error" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie)) - - val promiseOnionAddress = Promise[OnionAddress]() - - val protocolHandler = TestActorRef(props( - version = "v2", - privateKeyPath = pkFile, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "server-hash-error") - - protocolHandler ! Connected(LocalHost, LocalHost) - - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) - - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50D0 SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(Failure(TorException("Unexpected server hash"))) - } - } - - "v2 should handle AUTHENTICATE failure" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie)) - - val promiseOnionAddress = Promise[OnionAddress]() - - val protocolHandler = TestActorRef(props( - version = "v2", - privateKeyPath = pkFile, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "auth-error") - - protocolHandler ! Connected(LocalHost, LocalHost) - - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) - - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) - protocolHandler ! ByteString( - "515 Authentication failed: Safe cookie response did not match expected value.\r\n" - ) - expectMsg(Failure(TorException("Tor server returned error: 515 Authentication failed: Safe cookie response did not match expected value."))) - } - } - - "v2 should handle ADD_ONION failure" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie)) - - val promiseOnionAddress = Promise[OnionAddress]() - - val protocolHandler = TestActorRef(props( - version = "v2", - privateKeyPath = pkFile, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "add-onion-error") - - protocolHandler ! Connected(LocalHost, LocalHost) - - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) - - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) - protocolHandler ! ByteString( - "250 OK\r\n" - ) - - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) - protocolHandler ! ByteString( - "513 Invalid argument\r\n" - ) - - expectMsg(Failure(TorException("Tor server returned error: 513 Invalid argument"))) - } - } - def withTempDir[T](f: String => T): T = { val d = Files.createTempDirectory("test-") try { From 5e8d57389a07871eda5f1498022624f990c4ffbe Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 1 Feb 2019 16:09:28 +0100 Subject: [PATCH 24/40] replacing auth SAFECOOKIE->PASSWORD --- TOR.md | 21 +++- eclair-core/src/main/resources/reference.conf | 1 + .../main/scala/fr/acinq/eclair/Setup.scala | 4 +- .../acinq/eclair/tor/TorProtocolHandler.scala | 102 +++-------------- .../scala/fr/acinq/eclair/TestUtils.scala | 15 +++ .../eclair/tor/TorProtocolHandlerSpec.scala | 107 ++++++------------ 6 files changed, 83 insertions(+), 167 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala diff --git a/TOR.md b/TOR.md index 4d04a283b7..a2ea458736 100644 --- a/TOR.md +++ b/TOR.md @@ -18,25 +18,34 @@ brew install tor For Windows: -Download the "Expert Bundle" from https://www.torproject.org/download/download.html and extract it to the root of your drive (e.g. `C:\tor-win32-0.3.5.7`). +Download the "Expert Bundle" from https://www.torproject.org/download/download.html and extract it to the root of your drive (e.g. `C:\tor`). + +### Configuring Tor + +First pick a password and hash it with this command: + +```shell +$ tor --hash-password this-is-an-example-password-change-it +16:94A50709CAA98333602756426F43E6AC6760B9ADEF217F58219E639E5A +``` Edit Tor configuration file: - `/etc/tor/torrc` (Linux) - `/usr/local/etc/tor/torrc` (Mac OS X) - - `C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\tor\torrc` (Windows) + - `C:\tor\conf\torrc` (Windows) + +Replace the value for `HashedControlPassword` with the result of the command above. ``` SOCKSPort 9050 ControlPort 9051 -CookieAuthentication 1 +HashedControlPassword 16:--REPLACE--THIS--WITH--THE--HASH--OF--YOUR--PASSWORD ExitPolicy reject *:* ``` -Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled. +Eclair requires password authentication as well as SOCKS5 and control connections to be enabled. Change the value of the `ExitPolicy` parameter only if you really know what you are doing. -Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`). - ### Start Tor For Linux: diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 28bfca0898..6cf35f45c8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -108,6 +108,7 @@ eclair { tor { enabled = false + password = "foobar" host = "127.0.0.1" port = 9051 private-key-file = "tor.dat" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index dd0b8ade07..cbdbc0a593 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -314,10 +314,10 @@ class Setup(datadir: File, if (config.getBoolean("tor.enabled")) { val promiseTorAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( + password = config.getString("tor.password"), privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, virtualPort = config.getInt("server.port"), - onionAdded = Some(promiseTorAddress), - nonce = None) + onionAdded = Some(promiseTorAddress)) val controller = system.actorOf(SimpleSupervisor.props(Controller.props( address = new InetSocketAddress(config.getString("tor.host"), config.getInt("tor.port")), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 9221119767..4e9b494e59 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -7,10 +7,8 @@ import java.nio.file.{FileSystems, Files, Paths} import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString -import fr.acinq.eclair.randomBytes import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec -import javax.xml.bind.DatatypeConverter import scala.concurrent.Promise @@ -21,17 +19,17 @@ case class TorException(msg: String) extends RuntimeException(msg) * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * + * @param password Tor controller password * @param privateKeyPath path to a file that contains a Tor private key * @param virtualPort port of our protected local server (typically 9735) * @param targetPorts target ports of the public hidden service * @param onionAdded a Promise to track creation of the endpoint - * @param clientNonce optional client nonce, will be randomly generated if omitted */ -class TorProtocolHandler(privateKeyPath: String, +class TorProtocolHandler(password: String, + privateKeyPath: String, virtualPort: Int, targetPorts: Seq[Int], - onionAdded: Option[Promise[OnionAddress]], - clientNonce: Option[Array[Byte]] + onionAdded: Option[Promise[OnionAddress]] ) extends Actor with Stash with ActorLogging { import TorProtocolHandler._ @@ -40,8 +38,6 @@ class TorProtocolHandler(privateKeyPath: String, private var address: Option[OnionAddress] = None - private val nonce: Array[Byte] = clientNonce.getOrElse(randomBytes(32)) - override def receive: Receive = { case Connected(_, _) => receiver = sender() @@ -52,28 +48,12 @@ class TorProtocolHandler(privateKeyPath: String, def protocolInfo: Receive = { case data: ByteString => val res = parseResponse(readResponse(data)) - val protoInfo = ProtocolInfo( - methods = res.getOrElse("METHODS", throw TorException("Tor auth methods not found")), - cookieFile = unquote(res.getOrElse("COOKIEFILE", throw TorException("Tor cookie file not found"))), - version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found")))) - log.info(s"Tor version ${protoInfo.version}") - sendCommand(s"AUTHCHALLENGE SAFECOOKIE ${hex(nonce)}") - context become authChallenge(protoInfo.cookieFile) - } - - def authChallenge(cookieFile: String): Receive = { - case data: ByteString => - val res = parseResponse(readResponse(data)) - val clientHash = computeClientHash( - res.getOrElse("SERVERHASH", throw TorException("Tor server hash not found")), - res.getOrElse("SERVERNONCE", throw TorException("Tor server nonce not found")), - cookieFile - ) - sendCommand(s"AUTHENTICATE ${hex(clientHash)}") + val version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found"))) + log.info(s"Tor version $version ") + sendCommand(s"""AUTHENTICATE "$password"""") context become authenticate } - def authenticate: Receive = { case data: ByteString => readResponse(data) @@ -92,6 +72,13 @@ class TorProtocolHandler(privateKeyPath: String, } } + + override def aroundReceive(receive: Receive, msg: Any): Unit = try { + super.aroundReceive(receive, msg) + } catch { + case t: Throwable => onionAdded.map(_.tryFailure(t)) + } + override def unhandled(message: Any): Unit = message match { case GetOnionAddress => sender() ! address @@ -123,45 +110,19 @@ class TorProtocolHandler(privateKeyPath: String, } } - private def computeClientHash(serverHash: String, serverNonce: String, cookieFile: String): Array[Byte] = { - val decodedServerHash = unhex(serverHash) - if (decodedServerHash.length != 32) - throw TorException("Invalid server hash length") - - val decodedServerNonce = unhex(serverNonce) - if (decodedServerNonce.length != 32) - throw TorException("Invalid server nonce length") - - val cookie = readBytes(cookieFile, 32) - - val message = cookie ++ nonce ++ decodedServerNonce - - val computedServerHash = hex(hmacSHA256(ServerKey, message)) - if (computedServerHash != serverHash) { - throw TorException("Unexpected server hash") - } - - hmacSHA256(ClientKey, message) - } - private def sendCommand(cmd: String): Unit = { receiver ! ByteString(s"$cmd\r\n") } - - override def postStop(): Unit = { - super.postStop() - onionAdded.foreach(_.tryFailure(new RuntimeException("tor connection exception"))) - } } object TorProtocolHandler { - def props(privateKeyPath: String, + def props(password: String, + privateKeyPath: String, virtualPort: Int, targetPorts: Seq[Int] = Seq(), - onionAdded: Option[Promise[OnionAddress]] = None, - nonce: Option[Array[Byte]] = None + onionAdded: Option[Promise[OnionAddress]] = None ): Props = - Props(new TorProtocolHandler(privateKeyPath, virtualPort, targetPorts, onionAdded, nonce)) + Props(new TorProtocolHandler(password, privateKeyPath, virtualPort, targetPorts, onionAdded)) // those are defined in the spec private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() @@ -169,29 +130,6 @@ object TorProtocolHandler { case object GetOnionAddress - case class ProtocolInfo(methods: String, cookieFile: String, version: String) - - def readBytes(filename: String, n: Int): Array[Byte] = { - val bytes = Array.ofDim[Byte](1024) - val s = new FileInputStream(filename) - try { - if (s.read(bytes) != n) - throw TorException("Invalid file length") - bytes.take(n) - } finally { - s.close() - } - } - - def writeBytes(filename: String, bytes: Array[Byte]): Unit = { - val s = new FileOutputStream(filename) - try { - s.write(bytes) - } finally { - s.close() - } - } - def readString(filename: String): String = { val r = new BufferedReader(new FileReader(filename)) try { @@ -225,10 +163,6 @@ object TorProtocolHandler { .replace("""\\""", """\""") .replace("""\"""", "\"") - def hex(buf: Seq[Byte]): String = buf.map("%02X" format _).mkString - - def unhex(hexString: String): Array[Byte] = DatatypeConverter.parseHexBinary(hexString) - private val r1 = """(\d+)\-(.*)""".r private val r2 = """(\d+) (.*)""".r diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala new file mode 100644 index 0000000000..acd772b83c --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -0,0 +1,15 @@ +package fr.acinq.eclair + +import java.io.File + +object TestUtils { + + /** + * Get the module's target directory (works from command line and within intellij) + */ + val BUILD_DIRECTORY = sys + .props + .get("buildDirectory") // this is defined if we run from maven + .getOrElse(new File(sys.props("user.dir"), "target").getAbsolutePath) // otherwise we probably are in intellij, so we build it manually assuming that user.dir == path to the module + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index bec47b0c99..c2fefd3215 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -1,17 +1,15 @@ package fr.acinq.eclair.tor -import java.io.{File, IOException} +import java.io._ import java.net.InetSocketAddress -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} import akka.actor.ActorSystem -import akka.actor.Status.Failure import akka.io.Tcp.Connected import akka.testkit.{ImplicitSender, TestActorRef, TestKit} import akka.util.ByteString +import fr.acinq.eclair.TestUtils import fr.acinq.eclair.wire.NodeAddress -import org.scalatest.{BeforeAndAfterAll, MustMatchers, WordSpecLike} +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, MustMatchers, WordSpecLike} import scala.concurrent.duration._ import scala.concurrent.{Await, Promise} @@ -20,28 +18,29 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) with ImplicitSender with WordSpecLike with MustMatchers + with BeforeAndAfterEach with BeforeAndAfterAll { import TorProtocolHandler._ - override protected def afterAll(): Unit = { - super.afterAll() - system.terminate() - } - val LocalHost = new InetSocketAddress("localhost", 8888) - val ClientNonce = "8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA" - val AuthCookie = "AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485" + val Password = "foobar" + val PkFile = new File(TestUtils.BUILD_DIRECTORY,"testtor.dat") + + override protected def beforeEach(): Unit = { + super.afterEach() + PkFile.delete() + } "tor" ignore { val promiseOnionAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( - privateKeyPath = sys.props("user.home") + "/v2_pk", + password = Password, + privateKeyPath = new File(TestUtils.BUILD_DIRECTORY, "testtor.dat").getAbsolutePath, virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))) + onionAdded = Some(promiseOnionAddress)) val controller = TestActorRef(Controller.props(new InetSocketAddress("localhost", 9051), protocolHandlerProps), "tor") @@ -53,36 +52,26 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) } "happy path v3" in { - withTempDir { dir => - val cookieFile = dir + File.separator + "cookie" - val pkFile = dir + File.separator + "pk" - - writeBytes(cookieFile, unhex(AuthCookie)) val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(props( - privateKeyPath = pkFile, + password = Password, + privateKeyPath = PkFile.getAbsolutePath, virtualPort = 9999, - onionAdded = Some(promiseOnionAddress), - nonce = Some(unhex(ClientNonce))), "happy-v3") + onionAdded = Some(promiseOnionAddress)), "happy-v3") protocolHandler ! Connected(LocalHost, LocalHost) expectMsg(ByteString("PROTOCOLINFO 1\r\n")) protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + cookieFile + "\"\r\n" + + "250-AUTH METHODS=HASHEDPASSWORD\r\n" + "250-VERSION Tor=\"0.3.4.8\"\r\n" + "250 OK\r\n" ) - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) - protocolHandler ! ByteString( - "250 AUTHCHALLENGE SERVERHASH=6828E74049924F37CBC61F2AAD4DD78D8DC09BEF1B4C3BF6FF454016ED9D50DF SERVERNONCE=B4AA04B6E7E2DF60DCB0F62C264903346E05D1675E77795529E22CA90918DEE7\r\n" - ) - - expectMsg(ByteString("AUTHENTICATE 0DDCAB5DEB39876CDEF7AF7860A1C738953395349F43B99F4E5E0F131B0515DF\r\n")) + expectMsg(ByteString(s"""AUTHENTICATE "$Password"\r\n""")) protocolHandler ! ByteString( "250 OK\r\n" ) @@ -102,72 +91,40 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) address.toOnion must be ("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") NodeAddress(address).toString must be ("Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") - readString(pkFile) must be("ED25519-V3:private-key") - } + readString(PkFile.getAbsolutePath) must be("ED25519-V3:private-key") } - "v3 should not be supported by 0.3.3.5" in { - val protocolHandler = TestActorRef(props( - privateKeyPath = "", - virtualPort = 9999, - onionAdded = None, - nonce = Some(unhex(ClientNonce))), "unsupported-v3") - - protocolHandler ! Connected(LocalHost, LocalHost) + "v3 should handle AUTHENTICATE errors" in { - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"cookieFile\"\r\n" + - "250-VERSION Tor=\"0.3.3.5\"\r\n" + - "250 OK\r\n" - ) + val badPassword = "badpassword" - expectMsg(Failure(TorException("Tor version 0.3.3.5 does not support protocol V3"))) - } - - "v3 should handle AUTHCHALLENGE errors" in { + val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(props( + password = badPassword, privateKeyPath = "", virtualPort = 9999, - onionAdded = None, - nonce = Some(unhex(ClientNonce))), "authchallenge-error") + onionAdded = Some(promiseOnionAddress)), "authchallenge-error") protocolHandler ! Connected(LocalHost, LocalHost) expectMsg(ByteString("PROTOCOLINFO 1\r\n")) protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"cookieFile\"\r\n" + + "250-AUTH METHODS=HASHEDPASSWORD\r\n" + "250-VERSION Tor=\"0.3.4.8\"\r\n" + "250 OK\r\n" ) - expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA\r\n")) + expectMsg(ByteString(s"""AUTHENTICATE "$badPassword"\r\n""")) protocolHandler ! ByteString( - "513 Invalid base16 client nonce\r\n" + "515 Authentication failed: Password did not match HashedControlPassword *or* authentication cookie.\r\n" ) - expectMsg(Failure(TorException("Tor server returned error: 513 Invalid base16 client nonce"))) - } - - def withTempDir[T](f: String => T): T = { - val d = Files.createTempDirectory("test-") - try { - f(d.toString) - } finally { - Files.walkFileTree(d, new SimpleFileVisitor[Path]() { - override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { - Files.delete(file) - FileVisitResult.CONTINUE - } - - override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = { - Files.delete(dir) - FileVisitResult.CONTINUE - } - }) + intercept[TorException] { + Await.result(promiseOnionAddress.future, 3 seconds) } + } + } \ No newline at end of file From f32334e533b027ff9a90496be2a8125446f11cb6 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 1 Feb 2019 15:11:33 +0100 Subject: [PATCH 25/40] use java.nio.Path instead of String --- .../main/scala/fr/acinq/eclair/Setup.scala | 3 +- .../acinq/eclair/tor/TorProtocolHandler.scala | 31 +++++-------------- .../eclair/tor/TorProtocolHandlerSpec.scala | 13 ++++---- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index cbdbc0a593..1fa37ab8cc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair import java.io.File import java.net.InetSocketAddress +import java.nio.file.Paths import java.sql.DriverManager import java.util.concurrent.TimeUnit @@ -315,7 +316,7 @@ class Setup(datadir: File, val promiseTorAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( password = config.getString("tor.password"), - privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).getAbsolutePath, + privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath, virtualPort = config.getInt("server.port"), onionAdded = Some(promiseTorAddress)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 4e9b494e59..5882786550 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -2,7 +2,8 @@ package fr.acinq.eclair.tor import java.io._ import java.nio.file.attribute.PosixFilePermissions -import java.nio.file.{FileSystems, Files, Paths} +import java.nio.file.{FileSystems, Files, Path, Paths} +import java.util import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected @@ -26,7 +27,7 @@ case class TorException(msg: String) extends RuntimeException(msg) * @param onionAdded a Promise to track creation of the endpoint */ class TorProtocolHandler(password: String, - privateKeyPath: String, + privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int], onionAdded: Option[Promise[OnionAddress]] @@ -95,7 +96,7 @@ class TorProtocolHandler(password: String, } private def computeKey: String = { - if (Files.exists(Paths.get(privateKeyPath))) { + if (privateKeyPath.toFile.exists()) { readString(privateKeyPath) } else { "NEW:ED25519-V3" @@ -117,7 +118,7 @@ class TorProtocolHandler(password: String, object TorProtocolHandler { def props(password: String, - privateKeyPath: String, + privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int] = Seq(), onionAdded: Option[Promise[OnionAddress]] = None @@ -130,32 +131,16 @@ object TorProtocolHandler { case object GetOnionAddress - def readString(filename: String): String = { - val r = new BufferedReader(new FileReader(filename)) - try { - r.readLine() - } finally { - r.close() - } - } + def readString(path: Path): String = Files.readAllLines(path).get(0) - def writeString(filename: String, string: String): Unit = { - val w = new PrintWriter(new OutputStreamWriter(new FileOutputStream(filename))) - try { - w.print(string) - } finally { - w.close() - } - } + def writeString(path: Path, string: String): Unit = Files.write(path, util.Arrays.asList(string)) - def setPermissions(filename: String, permissionString: String): Unit = { - val path = FileSystems.getDefault.getPath(filename) + def setPermissions(path: Path, permissionString: String): Unit = try { Files.setPosixFilePermissions(path, PosixFilePermissions.fromString(permissionString)) } catch { case _: UnsupportedOperationException => () // we are on windows } - } def unquote(s: String): String = s .stripSuffix("\"") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index c2fefd3215..dfce37bbb3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -2,6 +2,7 @@ package fr.acinq.eclair.tor import java.io._ import java.net.InetSocketAddress +import java.nio.file.Paths import akka.actor.ActorSystem import akka.io.Tcp.Connected @@ -25,11 +26,11 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val LocalHost = new InetSocketAddress("localhost", 8888) val Password = "foobar" - val PkFile = new File(TestUtils.BUILD_DIRECTORY,"testtor.dat") + val PkFilePath = Paths.get(TestUtils.BUILD_DIRECTORY,"testtor.dat") override protected def beforeEach(): Unit = { super.afterEach() - PkFile.delete() + PkFilePath.toFile.delete() } "tor" ignore { @@ -38,7 +39,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val protocolHandlerProps = TorProtocolHandler.props( password = Password, - privateKeyPath = new File(TestUtils.BUILD_DIRECTORY, "testtor.dat").getAbsolutePath, + privateKeyPath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtor.dat"), virtualPort = 9999, onionAdded = Some(promiseOnionAddress)) @@ -57,7 +58,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val protocolHandler = TestActorRef(props( password = Password, - privateKeyPath = PkFile.getAbsolutePath, + privateKeyPath = PkFilePath, virtualPort = 9999, onionAdded = Some(promiseOnionAddress)), "happy-v3") @@ -91,7 +92,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) address.toOnion must be ("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") NodeAddress(address).toString must be ("Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") - readString(PkFile.getAbsolutePath) must be("ED25519-V3:private-key") + readString(PkFilePath) must be("ED25519-V3:private-key") } "v3 should handle AUTHENTICATE errors" in { @@ -102,7 +103,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val protocolHandler = TestActorRef(props( password = badPassword, - privateKeyPath = "", + privateKeyPath = PkFilePath, virtualPort = 9999, onionAdded = Some(promiseOnionAddress)), "authchallenge-error") From 5eba5db9eb02e37634c29b041889010d214819ed Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 1 Feb 2019 16:17:47 +0100 Subject: [PATCH 26/40] put socks proxy parameters in NodeParams --- .../scala/fr/acinq/eclair/NodeParams.scala | 19 ++++++++++++-- .../main/scala/fr/acinq/eclair/Setup.scala | 9 +------ .../scala/fr/acinq/eclair/io/Client.scala | 25 ++++++++++--------- .../main/scala/fr/acinq/eclair/io/Peer.scala | 10 +++----- .../fr/acinq/eclair/io/Switchboard.scala | 7 +++--- .../scala/fr/acinq/eclair/TestConstants.scala | 6 +++-- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- 7 files changed, 43 insertions(+), 35 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index b46345e9e3..77bab6dc75 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ +import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.wire.Color import scala.collection.JavaConversions._ @@ -81,7 +82,8 @@ case class NodeParams(keyManager: KeyManager, paymentRequestExpiry: FiniteDuration, maxPendingPaymentRequests: Int, maxPaymentFee: Double, - minFundingSatoshis: Long) { + minFundingSatoshis: Long, + socksProxy_opt: Option[Socks5ProxyParams]) { val privateKey = keyManager.nodeKey.privateKey val nodeId = keyManager.nodeId @@ -182,6 +184,18 @@ object NodeParams { (p -> (gf, lf)) }.toMap + val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { + Some(Socks5ProxyParams( + new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), + config.getBoolean("tor.stream-isolation"), + config.getBoolean("socks5.use-for-ipv4"), + config.getBoolean("socks5.use-for-ipv6"), + config.getBoolean("socks5.use-for-tor") + )) + } else { + None + } + NodeParams( keyManager = keyManager, alias = nodeAlias, @@ -226,7 +240,8 @@ object NodeParams { paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), maxPendingPaymentRequests = config.getInt("max-pending-payment-requests"), maxPaymentFee = config.getDouble("max-payment-fee"), - minFundingSatoshis = config.getLong("min-funding-satoshis") + minFundingSatoshis = config.getLong("min-funding-satoshis"), + socksProxy_opt = socksProxy_opt ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 1fa37ab8cc..38670c1bae 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -235,14 +235,7 @@ class Setup(datadir: File, register = system.actorOf(SimpleSupervisor.props(Props(new Register), "register", SupervisorStrategy.Resume)) relayer = system.actorOf(SimpleSupervisor.props(Relayer.props(nodeParams, register, paymentHandler), "relayer", SupervisorStrategy.Resume)) authenticator = system.actorOf(SimpleSupervisor.props(Authenticator.props(nodeParams), "authenticator", SupervisorStrategy.Resume)) - socksProxy = if (config.getBoolean("socks5.enabled")) Some(Socks5ProxyParams( - new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), - config.getBoolean("tor.stream-isolation"), - config.getBoolean("socks5.use-for-ipv4"), - config.getBoolean("socks5.use-for-ipv6"), - config.getBoolean("socks5.use-for-tor") - )) else None - switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy), "switchboard", SupervisorStrategy.Resume)) + switchboard = system.actorOf(SimpleSupervisor.props(Switchboard.props(nodeParams, authenticator, watcher, router, relayer, wallet), "switchboard", SupervisorStrategy.Resume)) server = system.actorOf(SimpleSupervisor.props(Server.props(nodeParams, authenticator, serverBindingAddress, Some(tcpBound)), "server", SupervisorStrategy.Restart)) paymentInitiator = system.actorOf(SimpleSupervisor.props(PaymentInitiator.props(nodeParams.nodeId, router, register), "payment-initiator", SupervisorStrategy.Restart)) _ = for (i <- 0 until config.getInt("autoprobe-count")) yield system.actorOf(SimpleSupervisor.props(Autoprobe.props(nodeParams, router, paymentInitiator), s"payment-autoprobe-$i", SupervisorStrategy.Restart)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index d14fac6294..4d4c1c6eef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -23,12 +23,11 @@ import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.io.Client.{ConnectionFailed, Socks5ProxyParams} +import fr.acinq.bitcoin.toHexString +import fr.acinq.eclair.io.Client.ConnectionFailed import fr.acinq.eclair.tor.Socks5Connection import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected} -import fr.acinq.eclair.{Logs, NodeParams} -import fr.acinq.eclair.randomBytes -import fr.acinq.bitcoin.toHexString +import fr.acinq.eclair.{Logs, NodeParams, randomBytes} import scala.concurrent.duration._ @@ -36,7 +35,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxyParams: Option[Socks5ProxyParams]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { import Tcp._ import context.system @@ -83,11 +82,13 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) context become connected(connection) case Some(_) => - val (username, password) = if (socksProxyParams.exists(_.randomizeCredentials)) - // randomize credentials for every proxy connection to enable Tor stream isolation - (Some(toHexString(randomBytes(16))), Some(toHexString(randomBytes(16)))) - else - (None, None) + val (username, password) = nodeParams.socksProxy_opt match { + case Some(_) => + // randomize credentials for every proxy connection to enable Tor stream isolation + (Some(toHexString(randomBytes(16))), Some(toHexString(randomBytes(16)))) + case None => + (None, None) + } connection = context.actorOf(Socks5Connection.props(sender(), username, password)) context watch connection connection ! Socks5Connect(remoteAddress) @@ -113,7 +114,7 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) - private def proxyAddress: Option[InetSocketAddress] = socksProxyParams.flatMap { proxyParams => + private def proxyAddress: Option[InetSocketAddress] = nodeParams.socksProxy_opt.flatMap { proxyParams => remoteAddress.getAddress match { case _ if remoteAddress.getHostString.endsWith(".onion") => if (proxyParams.useForTor) Some(proxyParams.address) else None case _: Inet4Address => if (proxyParams.useForIPv4) Some(proxyParams.address) else None @@ -127,7 +128,7 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine object Client extends App { - def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef], socksProxy: Option[Socks5ProxyParams]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt, socksProxy)) + def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 5fffca66f8..301b08bc22 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -27,8 +27,6 @@ import fr.acinq.bitcoin.{BinaryData, DeterministicWallet, MilliSatoshi, Protocol import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.router._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{wire, _} @@ -41,7 +39,7 @@ import scala.util.Random /** * Created by PM on 26/08/2016. */ -class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { +class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends FSMDiagnosticActorLogging[Peer.State, Peer.Data] { import Peer._ @@ -66,7 +64,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay } else { // we immediately process explicit connection requests to new addresses - context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = Some(sender()), socksProxy)) + context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = Some(sender()))) stay } @@ -75,7 +73,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor case None => stay // no-op (this peer didn't initiate the connection and doesn't have the ip of the counterparty) case _ if d.channels.isEmpty => stay // no-op (no more channels with this peer) case Some(address) => - context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = None, socksProxy)) + context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = None)) // exponential backoff retry with a finite max setTimer(RECONNECT_TIMER, Reconnect, Math.min(10 + Math.pow(2, d.attempts), 60) seconds, repeat = false) stay using d.copy(attempts = d.attempts + 1) @@ -496,7 +494,7 @@ object Peer { val IGNORE_NETWORK_ANNOUNCEMENTS_PERIOD = 5 minutes - def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy)) + def props(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Peer(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet)) // @formatter:off diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 1bc060afbf..f5db019c9c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.channel.{HasCommitments, _} -import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.payment.Relayer.RelayPayload import fr.acinq.eclair.payment.{Relayed, Relayer} import fr.acinq.eclair.router.Rebroadcast @@ -37,7 +36,7 @@ import scala.util.Success * Ties network connections to peers. * Created by PM on 14/02/2017. */ -class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) extends Actor with ActorLogging { +class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) extends Actor with ActorLogging { import Switchboard._ @@ -120,7 +119,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto case Some(peer) => peer case None => log.info(s"creating new peer current=${peers.size}") - val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet, socksProxy), name = s"peer-$remoteNodeId") + val peer = context.actorOf(Peer.props(nodeParams, remoteNodeId, authenticator, watcher, router, relayer, wallet), name = s"peer-$remoteNodeId") peer ! Peer.Init(previousKnownAddress, offlineChannels) context watch (peer) peer @@ -135,7 +134,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto object Switchboard extends Logging { - def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet, socksProxy: Option[Socks5ProxyParams]) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet, socksProxy)) + def props(nodeParams: NodeParams, authenticator: ActorRef, watcher: ActorRef, router: ActorRef, relayer: ActorRef, wallet: EclairWallet) = Props(new Switchboard(nodeParams, authenticator, watcher, router, relayer, wallet)) /** * If we have stopped eclair while it was forwarding HTLCs, it is possible that we are in a state were an incoming HTLC diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 8167d5178b..4d45bda5e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -86,7 +86,8 @@ object TestConstants { paymentRequestExpiry = 1 hour, maxPendingPaymentRequests = 10000000, maxPaymentFee = 0.03, - minFundingSatoshis = 1000L) + minFundingSatoshis = 1000L, + socksProxy_opt = None) def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, @@ -145,7 +146,8 @@ object TestConstants { paymentRequestExpiry = 1 hour, maxPendingPaymentRequests = 10000000, maxPaymentFee = 0.03, - minFundingSatoshis = 1000L) + minFundingSatoshis = 1000L, + socksProxy_opt = None) def channelParams = Peer.makeChannelParams( nodeParams = nodeParams, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index f0bd1f6f02..21ae51b30f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -37,7 +37,7 @@ class PeerSpec extends TestkitBaseClass { val transport = TestProbe() val wallet: EclairWallet = null // unused val remoteNodeId = Bob.nodeParams.nodeId - val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet, None)) + val peer = system.actorOf(Peer.props(Alice.nodeParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } From 0f8d898061a4986d5d56f529450717864b542feb Mon Sep 17 00:00:00 2001 From: pm47 Date: Sat, 2 Feb 2019 19:31:12 +0100 Subject: [PATCH 27/40] rework of socks5 client Instead of doing the error management ourselves, use the let-it-crash principle and let the supervisor handle and log failures. --- .../scala/fr/acinq/eclair/NodeParams.scala | 13 +- .../main/scala/fr/acinq/eclair/Setup.scala | 1 - .../scala/fr/acinq/eclair/io/Client.scala | 113 +++++-------- .../acinq/eclair/tor/Socks5Connection.scala | 156 ++++++++++-------- 4 files changed, 137 insertions(+), 146 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 77bab6dc75..8a0ac62e45 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -32,7 +32,7 @@ import fr.acinq.eclair.channel.Channel import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ -import fr.acinq.eclair.io.Client.Socks5ProxyParams +import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.wire.Color import scala.collection.JavaConversions._ @@ -186,11 +186,12 @@ object NodeParams { val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { Some(Socks5ProxyParams( - new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), - config.getBoolean("tor.stream-isolation"), - config.getBoolean("socks5.use-for-ipv4"), - config.getBoolean("socks5.use-for-ipv6"), - config.getBoolean("socks5.use-for-tor") + address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), + credentials_opt = None, + randomizeCredentials = config.getBoolean("tor.stream-isolation"), + useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), + useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), + useForTor = config.getBoolean("socks5.use-for-tor") )) } else { None diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 38670c1bae..723ee0d20d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -44,7 +44,6 @@ import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} import fr.acinq.eclair.blockchain.{EclairWallet, _} import fr.acinq.eclair.channel.Register import fr.acinq.eclair.crypto.LocalKeyManager -import fr.acinq.eclair.io.Client.Socks5ProxyParams import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 4d4c1c6eef..6ebbf41450 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -16,18 +16,17 @@ package fr.acinq.eclair.io -import java.net.{Inet4Address, Inet6Address, InetSocketAddress} +import java.net.InetSocketAddress import akka.actor.{Props, _} import akka.event.Logging.MDC import akka.io.Tcp.SO.KeepAlive import akka.io.{IO, Tcp} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.toHexString import fr.acinq.eclair.io.Client.ConnectionFailed -import fr.acinq.eclair.tor.Socks5Connection import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected} -import fr.acinq.eclair.{Logs, NodeParams, randomBytes} +import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams} +import fr.acinq.eclair.{Logs, NodeParams} import scala.concurrent.duration._ @@ -37,71 +36,54 @@ import scala.concurrent.duration._ */ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { - import Tcp._ import context.system // we could connect directly here but this allows to take advantage of the automated mdc configuration on message reception self ! 'connect - private var connection: ActorRef = _ - - def receive = { - + def receive: Receive = { case 'connect => - val addressToConnect = proxyAddress match { + val addressToConnect = nodeParams.socksProxy_opt.flatMap(proxyParams => Socks5ProxyParams.proxyAddress(remoteAddress, proxyParams)) match { + case Some(proxyAddress) => + log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") + proxyAddress case None => - log.info(s"connecting to pubkey=$remoteNodeId host=${remoteAddress.getHostString} port=${remoteAddress.getPort}") + log.info(s"connecting to ${str(remoteAddress)}") remoteAddress - case Some(socks5Address) => - log.info(s"connecting to SOCKS5 proxy ${str(socks5Address)}") - socks5Address - } - IO(Tcp) ! Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) - - case CommandFailed(_: Connect) => - proxyAddress match { - case None => - log.info(s"connection failed to $remoteNodeId@${str(remoteAddress)}") - case Some(socks5Address) => - log.info(s"connection failed to SOCKS5 proxy ${str(socks5Address)}") } - origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) - context stop self + IO(Tcp) ! Tcp.Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) + context become connecting(addressToConnect) + } - case CommandFailed(_: Socks5Connect) => - log.info(s"connection failed to $remoteNodeId@${str(remoteAddress)} via SOCKS5 ${proxyAddress.map(str).getOrElse("")}") + def connecting(to: InetSocketAddress): Receive = { + case Tcp.CommandFailed(_: Tcp.Connect) => + log.info(s"connection failed to ${str(to)}") origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self - case Connected(remote, _) => - proxyAddress match { - case None => - connection = sender() - context watch connection - log.info(s"connected to pubkey=$remoteNodeId host=${remote.getHostString} port=${remote.getPort}") - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) - context become connected(connection) - case Some(_) => - val (username, password) = nodeParams.socksProxy_opt match { - case Some(_) => - // randomize credentials for every proxy connection to enable Tor stream isolation - (Some(toHexString(randomBytes(16))), Some(toHexString(randomBytes(16)))) - case None => - (None, None) + case Tcp.Connected(peerOrProxyAddress, _) => + val connection = sender() + context watch connection + + nodeParams.socksProxy_opt match { + case Some(proxyParams) => + val proxyAddress = peerOrProxyAddress + log.info(s"connected to proxy ${str(proxyAddress)}") + val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(remoteAddress))) + context become { + case Tcp.CommandFailed(_: Socks5Connect) => + log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") + origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) + context stop self + case Socks5Connected(_) => + log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") + auth(proxy) + context become connected(proxy) } - connection = context.actorOf(Socks5Connection.props(sender(), username, password)) - context watch connection - connection ! Socks5Connect(remoteAddress) - } - - case Socks5Connected(_) => - proxyAddress match { - case Some(socks5Address) => - log.info(s"connected to pubkey=$remoteNodeId host=${remoteAddress.getHostString} port=${remoteAddress.getPort} via SOCKS5 proxy ${str(socks5Address)}") - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) + case None => + log.info(s"connected to ${str(remoteAddress)}") + auth(connection) context become connected(connection) - case _ => - log.error("Hmm.") } } @@ -110,28 +92,23 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine context stop self } - override def unhandled(message: Any): Unit = log.warning(s"unhandled message=$message") + override def unhandled(message: Any): Unit = { + log.warning(s"unhandled message=$message") + } - override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) + // we should not restart a failing socks client + override val supervisorStrategy = OneForOneStrategy(loggingEnabled = true) { case _ => SupervisorStrategy.Stop } - private def proxyAddress: Option[InetSocketAddress] = nodeParams.socksProxy_opt.flatMap { proxyParams => - remoteAddress.getAddress match { - case _ if remoteAddress.getHostString.endsWith(".onion") => if (proxyParams.useForTor) Some(proxyParams.address) else None - case _: Inet4Address => if (proxyParams.useForIPv4) Some(proxyParams.address) else None - case _: Inet6Address =>if (proxyParams.useForIPv6) Some(proxyParams.address) else None - case _ => None - } - } + override def mdc(currentMessage: Any): MDC = Logs.mdc(remoteNodeId_opt = Some(remoteNodeId)) private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}" + + def auth(connection: ActorRef) = authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) } -object Client extends App { +object Client { def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") - - case class Socks5ProxyParams(address: InetSocketAddress, randomizeCredentials: Boolean, useForIPv4: Boolean, useForIPv6: Boolean, useForTor: Boolean) - } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 595957a86e..c20c9af7bc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -5,82 +5,74 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} import akka.io.Tcp import akka.util.ByteString +import fr.acinq.bitcoin.toHexString +import fr.acinq.eclair.randomBytes +import fr.acinq.eclair.tor.Socks5Connection.{Credentials, Socks5Connect} /** + * Simple socks 5 client. It should be given a new connection, and will + * * Created by rorp * - * @param underlying underlying TcpConnection - * @param username user name for password authentication - * @param password password for password authentication + * @param connection underlying TcpConnection + * @param credentials_opt optional username/password for authentication */ -class Socks5Connection(underlying: ActorRef, username: Option[String], password: Option[String]) extends Actor with ActorLogging { - username.foreach(x => require(x.length < 256, "username is too long")) - password.foreach(x => require(x.length < 256, "password is too long")) +class Socks5Connection(connection: ActorRef, credentials_opt: Option[Credentials], command: Socks5Connect) extends Actor with ActorLogging { import fr.acinq.eclair.tor.Socks5Connection._ - private var commander: ActorRef = _ - private var handler: ActorRef = _ + context watch connection - context watch underlying + val passwordAuth: Boolean = credentials_opt.isDefined - def passwordAuth: Boolean = username.isDefined + var isConnected: Boolean = false - override def receive: Receive = { - case c@Socks5Connect(address) => - commander = sender() - context become greetings(c) - underlying ! Tcp.Register(self) - underlying ! Tcp.ResumeReading - underlying ! Tcp.Write(socks5Greeting(passwordAuth)) - } + connection ! Tcp.Register(self) + connection ! Tcp.ResumeReading + connection ! Tcp.Write(socks5Greeting(passwordAuth)) - def greetings(connectCommand: Socks5Connect): Receive = { + override def receive: Receive = greetings + + def greetings: Receive = { case Tcp.Received(data) => - handleExceptions(connectCommand) { if (data(0) != 0x05) { - throw new RuntimeException("Invalid SOCKS5 proxy response") + throw Socks5Error("Invalid SOCKS5 proxy response") } else if ((!passwordAuth && data(1) != NoAuth) || (passwordAuth && data(1) != PasswordAuth)) { - throw new RuntimeException("Unrecognized SOCKS5 auth method") + throw Socks5Error("Unrecognized SOCKS5 auth method") } else { if (data(1) == PasswordAuth) { - context become authenticate(connectCommand) - underlying ! Tcp.Write(socks5PasswordAuthenticationRequest( - username.getOrElse(throw new RuntimeException("username is not defined")), - password.getOrElse(throw new RuntimeException("password is not defined")))) - underlying ! Tcp.ResumeReading + context become authenticate + val credentials = credentials_opt.getOrElse(throw Socks5Error("credentials are not defined")) + connection ! Tcp.Write(socks5PasswordAuthenticationRequest(credentials.username, credentials.password)) + connection ! Tcp.ResumeReading } else { - context become connectionRequest(connectCommand) - underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) - underlying ! Tcp.ResumeReading + context become connectionRequest + connection ! Tcp.Write(socks5ConnectionRequest(command.address)) + connection ! Tcp.ResumeReading } } - }("Error connecting to SOCKS5 proxy") - } + } - def authenticate(connectCommand: Socks5Connect): Receive = { - case c@Tcp.Received(data) => - handleExceptions(connectCommand) { + def authenticate: Receive = { + case Tcp.Received(data) => if (data(0) != 0x01) { - throw new RuntimeException("Invalid SOCKS5 proxy response") + throw Socks5Error("Invalid SOCKS5 proxy response") } else if (data(1) != 0) { - throw new RuntimeException("SOCKS5 authentication failed") + throw Socks5Error("SOCKS5 authentication failed") } - context become connectionRequest(connectCommand) - underlying ! Tcp.Write(socks5ConnectionRequest(connectCommand.address)) - underlying ! Tcp.ResumeReading - }("SOCKS5 authentication error") - } + context become connectionRequest + connection ! Tcp.Write(socks5ConnectionRequest(command.address)) + connection ! Tcp.ResumeReading + } - def connectionRequest(connectCommand: Socks5Connect): Receive = { - case c@Tcp.Received(data) => - handleExceptions(connectCommand) { + def connectionRequest: Receive = { + case Tcp.Received(data) => if (data(0) != 0x05) { - throw new RuntimeException("Invalid SOCKS5 proxy response") + throw Socks5Error("Invalid SOCKS5 proxy response") } else { val status = data(1) if (status != 0) { - throw new RuntimeException(connectErrors.getOrElse(status, s"Unknown SOCKS5 error $status")) + throw Socks5Error(connectErrors.getOrElse(status, s"Unknown SOCKS5 error $status")) } val connectedAddress = data(3) match { case 0x01 => @@ -99,53 +91,54 @@ class Socks5Connection(underlying: ActorRef, username: Option[String], password: data.copyToArray(ip, 4, 4 + ip.length) val port = data(4 + ip.length).toInt << 8 | data(4 + ip.length + 1) new InetSocketAddress(InetAddress.getByAddress(ip), port) - case _ => throw new RuntimeException(s"Unrecognized address type") + case _ => throw Socks5Error(s"Unrecognized address type") } context become connected log.info(s"connected $connectedAddress") - commander ! Socks5Connected(connectedAddress) + context.parent ! Socks5Connected(connectedAddress) + isConnected = false } - }("Cannot establish SOCKS5 connection") } def connected: Receive = { - case Tcp.Register(actor, keepOpenOnPeerClosed, useResumeWriting) => - handler = actor - context become registered + case Tcp.Register(handler, _, _) => context become registered(handler) } - def registered: Receive = { - case c: Tcp.Command => underlying ! c + def registered(handler: ActorRef): Receive = { + case c: Tcp.Command => connection ! c case e: Tcp.Event => handler ! e } override def unhandled(message: Any): Unit = message match { - case Terminated(actor) if actor == underlying => - context stop self - case c: Tcp.ConnectionClosed => - commander ! c - context stop self - case _ => - log.warning(s"unhandled message=$message") + case Terminated(actor) if actor == connection => context stop self + case _: Tcp.ConnectionClosed => context stop self + case _ => log.warning(s"unhandled message=$message") } - private def handleExceptions[T](connectCommand: Socks5Connect)(f: => T)(message: => String): Unit = try { - f - } catch { - case e: Throwable => - log.error(e, message + " ") - underlying ! Tcp.Close - commander ! connectCommand.failureMessage + override def postStop(): Unit = { + super.postStop() + connection ! Tcp.Close + if (!isConnected) { + context.parent ! command.failureMessage + } } + } object Socks5Connection { - def props(tcpConnection: ActorRef, username: Option[String], password: Option[String]): Props = Props(new Socks5Connection(tcpConnection, username, password)) + def props(tcpConnection: ActorRef, credentials_opt: Option[Credentials], command: Socks5Connect): Props = Props(new Socks5Connection(tcpConnection, credentials_opt, command)) case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event case class Socks5Connect(address: InetSocketAddress) extends Tcp.Command + case class Socks5Error(message: String) extends RuntimeException(message) + + case class Credentials(username: String, password: String) { + require(username.length < 256, "username is too long") + require(password.length < 256, "password is too long") + } + val NoAuth: Byte = 0x00 val PasswordAuth: Byte = 0x02 @@ -190,7 +183,7 @@ object Socks5Connection { case a: Inet6Address => ByteString( 0x04 // IPv6 address ) ++ ByteString(a.getAddress) - case _ => throw new RuntimeException("Unknown InetAddress") + case _ => throw Socks5Error("Unknown InetAddress") } def addressToByteString(address: InetSocketAddress): ByteString = Option(address.getAddress) match { @@ -207,3 +200,24 @@ object Socks5Connection { def portToByteString(port: Int): ByteString = ByteString((port & 0x0000ff00) >> 8, port & 0x000000ff) } + +case class Socks5ProxyParams(address: InetSocketAddress, credentials_opt: Option[Credentials], randomizeCredentials: Boolean, useForIPv4: Boolean, useForIPv6: Boolean, useForTor: Boolean) + +object Socks5ProxyParams { + + def proxyAddress(remoteAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = + proxyParams.address.getAddress match { + case _ if remoteAddress.getHostString.endsWith(".onion") && proxyParams.useForTor => Some(proxyParams.address) + case _: Inet4Address if proxyParams.useForIPv4 => Some(proxyParams.address) + case _: Inet6Address if proxyParams.useForIPv6 => Some(proxyParams.address) + case _ => None + } + + def proxyCredentials(proxyParams: Socks5ProxyParams): Option[Socks5Connection.Credentials] = + if (proxyParams.randomizeCredentials) { + // randomize credentials for every proxy connection to enable Tor stream isolation + Some(Socks5Connection.Credentials(toHexString(randomBytes(16)), toHexString(randomBytes(16)))) + } else { + proxyParams.credentials_opt + } +} From 9602ea4a21c3128afa3a95c4dce7fcdc9fed314c Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 1 Feb 2019 19:22:22 +0100 Subject: [PATCH 28/40] using TestUtils.BUILD_DIRECTORY everywhere --- .../acinq/eclair/blockchain/bitcoind/BitcoindService.scala | 5 +++-- .../fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala | 4 ++-- .../eclair/interop/rustytests/SynchronizationPipe.scala | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala index 8c9cc28d2b..3eeaf0583e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoindService.scala @@ -25,6 +25,7 @@ import akka.pattern.pipe import akka.testkit.{TestKitBase, TestProbe} import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import com.softwaremill.sttp.okhttp.OkHttpFutureBackend +import fr.acinq.eclair.TestUtils import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.integration.IntegrationSpec import grizzled.slf4j.Logging @@ -41,10 +42,10 @@ trait BitcoindService extends Logging { import scala.sys.process._ - val INTEGRATION_TMP_DIR = s"${System.getProperty("buildDirectory")}/integration-${UUID.randomUUID().toString}" + val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") logger.info(s"using tmp dir: $INTEGRATION_TMP_DIR") - val PATH_BITCOIND = new File(System.getProperty("buildDirectory"), "bitcoin-0.16.3/bin/bitcoind") + val PATH_BITCOIND = new File(TestUtils.BUILD_DIRECTORY, "bitcoin-0.16.3/bin/bitcoind") val PATH_BITCOIND_DATADIR = new File(INTEGRATION_TMP_DIR, "datadir-bitcoin") var bitcoind: Process = null diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index 5a3ea6c0d8..a4f12f43f7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -21,7 +21,7 @@ import java.util.concurrent.{CountDownLatch, TimeUnit} import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{TestFSMRef, TestKit, TestProbe} -import fr.acinq.eclair.Globals +import fr.acinq.eclair.{Globals, TestUtils} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -75,7 +75,7 @@ class RustyTestsSpec extends TestKit(ActorSystem("test")) with Matchers with fix pipe ! new File(getClass.getResource(s"/scenarii/${test.name}.script").getFile) latch.await(30, TimeUnit.SECONDS) val ref = Source.fromFile(getClass.getResource(s"/scenarii/${test.name}.script.expected").getFile).getLines().filterNot(_.startsWith("#")).toList - val res = Source.fromFile(new File(s"${System.getProperty("buildDirectory")}/result.tmp")).getLines().filterNot(_.startsWith("#")).toList + val res = Source.fromFile(new File(TestUtils.BUILD_DIRECTORY, "result.tmp")).getLines().filterNot(_.startsWith("#")).toList withFixture(test.toNoArgTest(FixtureParam(ref, res))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 3c6b7223a8..9c6639e7e2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -21,6 +21,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.BinaryData +import fr.acinq.eclair.TestUtils import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IN, OUT} @@ -48,7 +49,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging val echo = "echo (.*)".r val dump = "(.):dump".r - val fout = new BufferedWriter(new FileWriter(s"${System.getProperty("buildDirectory")}/result.tmp")) + val fout = new BufferedWriter(new FileWriter(new File(TestUtils.BUILD_DIRECTORY, "result.tmp"))) def exec(script: List[String], a: ActorRef, b: ActorRef): Unit = { def resolve(x: String) = if (x == "A") a else b From ef7bea2e7e580786f1f762602bbda774b8ce02de Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 5 Feb 2019 14:45:05 +0100 Subject: [PATCH 29/40] add back v2 support and cookie auth --- TOR.md | 16 +- eclair-core/src/main/resources/reference.conf | 4 +- .../main/scala/fr/acinq/eclair/Setup.scala | 8 +- .../acinq/eclair/tor/TorProtocolHandler.scala | 140 +++++++-- .../eclair/tor/TorProtocolHandlerSpec.scala | 265 ++++++++++++++---- 5 files changed, 358 insertions(+), 75 deletions(-) diff --git a/TOR.md b/TOR.md index a2ea458736..6ad663d558 100644 --- a/TOR.md +++ b/TOR.md @@ -2,8 +2,6 @@ ### Installing Tor on your node -Eclair requires Tor 0.3.3.6+. - For Linux: ```shell @@ -83,6 +81,20 @@ eclair-cli getinfo Eclair saves the Tor endpoint's private key in `~/.eclair/tor_pk`, so that it can recreate the endpoint address after restart. If you remove the private key eclair will regenerate the endpoint address. +There are two possible values for `protocol-version`: + +``` +eclair.tor.protocol-version = "v3" +``` + +value | description +--------|--------------------------------------------------------- + v2 | set up a Tor hidden service version 2 end point + v3 | set up a Tor hidden service version 3 end point (default) + +Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports +both v2 and v3 addresses. + To create a new Tor circuit for every connection, use `stream-isolation` parameter: ``` diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 6cf35f45c8..d871f43091 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -108,7 +108,9 @@ eclair { tor { enabled = false - password = "foobar" + protocol = "v3" // v2, v3 + auth = "safecookie" // safecookie, password + password = "foobar" // used when auth=password host = "127.0.0.1" port = 9051 private-key-file = "tor.dat" diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 723ee0d20d..c25c0544b6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -47,6 +47,7 @@ import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ +import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion import fr.acinq.eclair.tor.{Controller, OnionAddress, TorProtocolHandler} import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray @@ -306,8 +307,13 @@ class Setup(datadir: File, private def initTor(): Option[InetSocketAddress] = { if (config.getBoolean("tor.enabled")) { val promiseTorAddress = Promise[OnionAddress]() + val auth = config.getString("tor.auth") match { + case "password" => TorProtocolHandler.Password(config.getString("tor.password")) + case "safecookie" => TorProtocolHandler.SafeCookie() + } val protocolHandlerProps = TorProtocolHandler.props( - password = config.getString("tor.password"), + version = OnionServiceVersion(config.getString("tor.protocol")), + authentication = auth, privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath, virtualPort = config.getInt("server.port"), onionAdded = Some(promiseTorAddress)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 5882786550..340ff50a14 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -1,32 +1,36 @@ package fr.acinq.eclair.tor -import java.io._ import java.nio.file.attribute.PosixFilePermissions -import java.nio.file.{FileSystems, Files, Path, Paths} +import java.nio.file.{Files, Path, Paths} import java.util import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.io.Tcp.Connected import akka.util.ByteString +import fr.acinq.bitcoin.BinaryData +import fr.acinq.eclair.tor.TorProtocolHandler.{Authentication, OnionServiceVersion} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import scala.concurrent.Promise +import scala.util.Try -case class TorException(msg: String) extends RuntimeException(msg) +case class TorException(private val msg: String) extends RuntimeException(s"Tor error: $msg") /** * Created by rorp * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * - * @param password Tor controller password - * @param privateKeyPath path to a file that contains a Tor private key - * @param virtualPort port of our protected local server (typically 9735) - * @param targetPorts target ports of the public hidden service - * @param onionAdded a Promise to track creation of the endpoint + * @param onionServiceVersion v2 or v3 + * @param authentication Tor controller auth mechanism (password or safecookie) + * @param privateKeyPath path to a file that contains a Tor private key + * @param virtualPort port of our protected local server (typically 9735) + * @param targetPorts target ports of the public hidden service + * @param onionAdded a Promise to track creation of the endpoint */ -class TorProtocolHandler(password: String, +class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, + authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int], @@ -49,9 +53,36 @@ class TorProtocolHandler(password: String, def protocolInfo: Receive = { case data: ByteString => val res = parseResponse(readResponse(data)) - val version = unquote(res.getOrElse("Tor", throw TorException("Tor version not found"))) - log.info(s"Tor version $version ") - sendCommand(s"""AUTHENTICATE "$password"""") + val methods: String = res.getOrElse("METHODS", throw TorException("auth methods not found")) + val torVersion = unquote(res.getOrElse("Tor", throw TorException("version not found"))) + log.info(s"Tor version $torVersion") + if (!OnionServiceVersion.isCompatible(onionServiceVersion, torVersion)) { + throw TorException(s"version $torVersion does not support onion service $onionServiceVersion") + } + if (!Authentication.isCompatible(authentication, methods)) { + throw TorException(s"cannot use authentication '$authentication', supported methods are '$methods'") + } + authentication match { + case Password(password) => + sendCommand(s"""AUTHENTICATE "$password"""") + context become authenticate + case SafeCookie(nonce) => + val cookieFile = Paths.get(unquote(res.getOrElse("COOKIEFILE", throw TorException("cookie file not found")))) + sendCommand(s"AUTHCHALLENGE SAFECOOKIE $nonce") + context become cookieChallenge(cookieFile, nonce) + } + } + + def cookieChallenge(cookieFile: Path, clientNonce: BinaryData): Receive = { + case data: ByteString => + val res = parseResponse(readResponse(data)) + val clientHash = computeClientHash( + res.getOrElse("SERVERHASH", throw TorException("server hash not found")), + res.getOrElse("SERVERNONCE", throw TorException("server nonce not found")), + clientNonce, + cookieFile + ) + sendCommand(s"AUTHENTICATE $clientHash") context become authenticate } @@ -67,7 +98,10 @@ class TorProtocolHandler(password: String, val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) - address = Some(OnionAddressV3(serviceId, virtualPort)) + address = Some(onionServiceVersion match { + case V2 => OnionAddressV2(serviceId, virtualPort) + case V3 => OnionAddressV3(serviceId, virtualPort) + }) onionAdded.foreach(_.success(address.get)) log.debug(s"Onion address: ${address.get}") } @@ -86,7 +120,7 @@ class TorProtocolHandler(password: String, } private def processOnionResponse(res: Map[String, String]): String = { - val serviceId = res.getOrElse("ServiceID", throw TorException("Tor service ID not found")) + val serviceId = res.getOrElse("ServiceID", throw TorException("service ID not found")) val privateKey = res.get("PrivateKey") privateKey.foreach { pk => writeString(privateKeyPath, pk) @@ -99,7 +133,10 @@ class TorProtocolHandler(password: String, if (privateKeyPath.toFile.exists()) { readString(privateKeyPath) } else { - "NEW:ED25519-V3" + onionServiceVersion match { + case V2 => "NEW:RSA1024" + case V3 => "NEW:ED25519-V3" + } } } @@ -111,24 +148,87 @@ class TorProtocolHandler(password: String, } } + private def computeClientHash(serverHash: BinaryData, serverNonce: BinaryData, clientNonce: BinaryData, cookieFile: Path): BinaryData = { + if (serverHash.length != 32) + throw TorException("invalid server hash length") + if (serverNonce.length != 32) + throw TorException("invalid server nonce length") + + val cookie = Files.readAllBytes(cookieFile) + + val message = cookie ++ clientNonce ++ serverNonce + + val computedServerHash = hmacSHA256(ServerKey, message) + if (computedServerHash != serverHash) { + throw TorException("unexpected server hash") + } + + hmacSHA256(ClientKey, message) + } + private def sendCommand(cmd: String): Unit = { receiver ! ByteString(s"$cmd\r\n") } } object TorProtocolHandler { - def props(password: String, + def props(version: OnionServiceVersion, + authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int] = Seq(), onionAdded: Option[Promise[OnionAddress]] = None ): Props = - Props(new TorProtocolHandler(password, privateKeyPath, virtualPort, targetPorts, onionAdded)) + Props(new TorProtocolHandler(version, authentication, privateKeyPath, virtualPort, targetPorts, onionAdded)) // those are defined in the spec private val ServerKey: Array[Byte] = "Tor safe cookie authentication server-to-controller hash".getBytes() private val ClientKey: Array[Byte] = "Tor safe cookie authentication controller-to-server hash".getBytes() + // @formatter:off + sealed trait OnionServiceVersion + case object V2 extends OnionServiceVersion + case object V3 extends OnionServiceVersion + // @formatter:on + + object OnionServiceVersion { + def apply(s: String): OnionServiceVersion = s match { + case "v2" | "V2" => V2 + case "v3" | "V3" => V3 + case _ => throw TorException(s"unknown protocol version `$s`") + } + + def isCompatible(onionServiceVersion: OnionServiceVersion, torVersion: String): Boolean = + onionServiceVersion match { + case V2 => true + case V3 => torVersion + .split("\\.") + .map(_.split('-').head) // remove non-numeric symbols at the end of the last number (rc, beta, alpha, etc.) + .map(d => Try(d.toInt).getOrElse(0)) + .zipAll(List(0, 3, 3, 6), 0, 0) // min version for v3 is 0.3.3.6 + .foldLeft(Option.empty[Boolean]) { // compare subversion by subversion starting from the left + case (Some(res), _) => Some(res) // we stop the comparison as soon as there is a difference + case (None, (v, vref)) => if (v > vref) Some(true) else if (v < vref) Some(false) else None + } + .getOrElse(true) // if version == 0.3.3.6 then result will be None + + } + } + + // @formatter:off + sealed trait Authentication + case class Password(password: String) extends Authentication { override def toString = "password" } + case class SafeCookie(nonce: BinaryData = fr.acinq.eclair.randomBytes(32)) extends Authentication { override def toString = "safecookie" } + // @formatter:on + + object Authentication { + def isCompatible(authentication: Authentication, methods: String): Boolean = + authentication match { + case _: Password => methods.contains("HASHEDPASSWORD") + case _: SafeCookie => methods.contains("SAFECOOKIE") + } + } + case object GetOnionAddress def readString(path: Path): String = Files.readAllLines(path).get(0) @@ -158,10 +258,10 @@ object TorProtocolHandler { .map { case r1(c, msg) => (c.toInt, msg) case r2(c, msg) => (c.toInt, msg) - case x@_ => throw TorException(s"Unknown response line format: `$x`") + case x@_ => throw TorException(s"unknown response line format: `$x`") } if (!ok(lines)) { - throw TorException(s"Tor server returned error: ${status(lines)} ${reason(lines)}") + throw TorException(s"server returned error: ${status(lines)} ${reason(lines)}") } lines } @@ -184,7 +284,7 @@ object TorProtocolHandler { }.toMap } - def hmacSHA256(key: Array[Byte], message: Array[Byte]): Array[Byte] = { + def hmacSHA256(key: Array[Byte], message: Array[Byte]): BinaryData = { val mac = Mac.getInstance("HmacSHA256") val secretKey = new SecretKeySpec(key, "HmacSHA256") mac.init(secretKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index dfce37bbb3..bdef62dd59 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -1,45 +1,47 @@ package fr.acinq.eclair.tor -import java.io._ import java.net.InetSocketAddress -import java.nio.file.Paths +import java.nio.file.{Files, Paths} import akka.actor.ActorSystem import akka.io.Tcp.Connected import akka.testkit.{ImplicitSender, TestActorRef, TestKit} import akka.util.ByteString +import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.TestUtils import fr.acinq.eclair.wire.NodeAddress -import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, MustMatchers, WordSpecLike} +import org.scalatest._ import scala.concurrent.duration._ import scala.concurrent.{Await, Promise} class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) + with FunSuiteLike with ImplicitSender - with WordSpecLike - with MustMatchers with BeforeAndAfterEach with BeforeAndAfterAll { import TorProtocolHandler._ val LocalHost = new InetSocketAddress("localhost", 8888) - val Password = "foobar" - val PkFilePath = Paths.get(TestUtils.BUILD_DIRECTORY,"testtor.dat") + val PASSWORD = "foobar" + val ClientNonce = "8969A7F3C03CD21BFD1CC49DBBD8F398345261B5B66319DF76BB2FDD8D96BCCA" + val PkFilePath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtorpk.dat") + val CookieFilePath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtorcookie.dat") + val AuthCookie = "AA8593C52DF9713CC5FF6A1D0A045B3FADCAE57745B1348A62A6F5F88D940485" override protected def beforeEach(): Unit = { super.afterEach() PkFilePath.toFile.delete() } - "tor" ignore { - + ignore("connect to real tor daemon") { val promiseOnionAddress = Promise[OnionAddress]() val protocolHandlerProps = TorProtocolHandler.props( - password = Password, - privateKeyPath = Paths.get(TestUtils.BUILD_DIRECTORY, "testtor.dat"), + version = OnionServiceVersion("v2"), + authentication = Password(PASSWORD), + privateKeyPath = PkFilePath, virtualPort = 9999, onionAdded = Some(promiseOnionAddress)) @@ -52,80 +54,241 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) println(NodeAddress(address)) } - "happy path v3" in { + test("happy path v2") { + val promiseOnionAddress = Promise[OnionAddress]() - val promiseOnionAddress = Promise[OnionAddress]() + val protocolHandler = TestActorRef(props( + version = OnionServiceVersion("v2"), + authentication = Password(PASSWORD), + privateKeyPath = PkFilePath, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress))) - val protocolHandler = TestActorRef(props( - password = Password, - privateKeyPath = PkFilePath, - virtualPort = 9999, - onionAdded = Some(promiseOnionAddress)), "happy-v3") + protocolHandler ! Connected(LocalHost, LocalHost) - protocolHandler ! Connected(LocalHost, LocalHost) + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=HASHEDPASSWORD\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) - expectMsg(ByteString("PROTOCOLINFO 1\r\n")) - protocolHandler ! ByteString( - "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=HASHEDPASSWORD\r\n" + - "250-VERSION Tor=\"0.3.4.8\"\r\n" + - "250 OK\r\n" - ) + expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n""")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) - expectMsg(ByteString(s"""AUTHENTICATE "$Password"\r\n""")) - protocolHandler ! ByteString( + expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "250-ServiceID=z4zif3fy7fe7bpg3\r\n" + + "250-PrivateKey=RSA1024:private-key\r\n" + "250 OK\r\n" - ) + ) + protocolHandler ! GetOnionAddress + expectMsg(Some(OnionAddressV2("z4zif3fy7fe7bpg3", 9999))) - expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) - protocolHandler ! ByteString( - "250-ServiceID=mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd\r\n" + - "250-PrivateKey=ED25519-V3:private-key\r\n" + - "250 OK\r\n" - ) + val address = Await.result(promiseOnionAddress.future, 3 seconds) + assert(address === OnionAddressV2("z4zif3fy7fe7bpg3", 9999)) + assert(address.toOnion === "z4zif3fy7fe7bpg3.onion:9999") + assert(NodeAddress(address).toString === "Tor2(cf3282ecb8f949f0bcdb,9999)") + + assert(readString(PkFilePath) === "RSA1024:private-key") + } - protocolHandler ! GetOnionAddress - expectMsg(Some(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) + test("happy path v3") { + val promiseOnionAddress = Promise[OnionAddress]() - val address = Await.result(promiseOnionAddress.future, 3 seconds) - address must be(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) - address.toOnion must be ("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") - NodeAddress(address).toString must be ("Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") + val protocolHandler = TestActorRef(props( + version = OnionServiceVersion("v3"), + authentication = Password(PASSWORD), + privateKeyPath = PkFilePath, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress))) - readString(PkFilePath) must be("ED25519-V3:private-key") + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=HASHEDPASSWORD\r\n" + + "250-VERSION Tor=\"0.3.4.8\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\r\n""")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) + + expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "250-ServiceID=mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd\r\n" + + "250-PrivateKey=ED25519-V3:private-key\r\n" + + "250 OK\r\n" + ) + + protocolHandler ! GetOnionAddress + expectMsg(Some(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) + + val address = Await.result(promiseOnionAddress.future, 3 seconds) + assert(address === OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) + assert(address.toOnion === "mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") + assert(NodeAddress(address).toString === "Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") + + assert(readString(PkFilePath) === "ED25519-V3:private-key") } - "v3 should handle AUTHENTICATE errors" in { + test("v2/v3 compatibility check against tor version") { + assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6")) + assert(!OnionServiceVersion.isCompatible(V3, "0.3.3.5")) + assert(OnionServiceVersion.isCompatible(V3, "0.3.3.6-devel")) + assert(OnionServiceVersion.isCompatible(V3, "0.4")) + assert(!OnionServiceVersion.isCompatible(V3, "0.2")) + assert(OnionServiceVersion.isCompatible(V3, "0.5.1.2.3.4")) - val badPassword = "badpassword" + } + test("authentication method errors") { val promiseOnionAddress = Promise[OnionAddress]() val protocolHandler = TestActorRef(props( - password = badPassword, + version = OnionServiceVersion("v2"), + authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, - onionAdded = Some(promiseOnionAddress)), "authchallenge-error") + onionAdded = Some(promiseOnionAddress))) protocolHandler ! Connected(LocalHost, LocalHost) expectMsg(ByteString("PROTOCOLINFO 1\r\n")) protocolHandler ! ByteString( "250-PROTOCOLINFO 1\r\n" + - "250-AUTH METHODS=HASHEDPASSWORD\r\n" + - "250-VERSION Tor=\"0.3.4.8\"\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + "250 OK\r\n" ) - expectMsg(ByteString(s"""AUTHENTICATE "$badPassword"\r\n""")) + assert(intercept[TorException] { + Await.result(promiseOnionAddress.future, 3 seconds) + } === TorException("cannot use authentication 'password', supported methods are 'COOKIE,SAFECOOKIE'")) + } + + test("invalid server hash") { + val promiseOnionAddress = Promise[OnionAddress]() + + Files.write(CookieFilePath, fr.acinq.eclair.randomBytes(32)) + + val protocolHandler = TestActorRef(props( + version = OnionServiceVersion("v2"), + authentication = SafeCookie(ClientNonce), + privateKeyPath = PkFilePath, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress))) + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) protocolHandler ! ByteString( - "515 Authentication failed: Password did not match HashedControlPassword *or* authentication cookie.\r\n" + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n" + ) + + assert(intercept[TorException] { + Await.result(promiseOnionAddress.future, 3 seconds) + } === TorException("unexpected server hash")) + } + + + test("AUTHENTICATE failure") { + val promiseOnionAddress = Promise[OnionAddress]() + + Files.write(CookieFilePath, BinaryData(AuthCookie)) + + val protocolHandler = TestActorRef(props( + version = OnionServiceVersion("v2"), + authentication = SafeCookie(ClientNonce), + privateKeyPath = PkFilePath, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress))) + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n")) + protocolHandler ! ByteString( + "515 Authentication failed: Safe cookie response did not match expected value.\r\n" ) - intercept[TorException] { + assert(intercept[TorException] { + Await.result(promiseOnionAddress.future, 3 seconds) + } === TorException("server returned error: 515 Authentication failed: Safe cookie response did not match expected value.")) + } + + test("ADD_ONION failure") { + val promiseOnionAddress = Promise[OnionAddress]() + + Files.write(CookieFilePath, BinaryData(AuthCookie)) + + val protocolHandler = TestActorRef(props( + version = OnionServiceVersion("v2"), + authentication = SafeCookie(ClientNonce), + privateKeyPath = PkFilePath, + virtualPort = 9999, + onionAdded = Some(promiseOnionAddress))) + + protocolHandler ! Connected(LocalHost, LocalHost) + + expectMsg(ByteString("PROTOCOLINFO 1\r\n")) + protocolHandler ! ByteString( + "250-PROTOCOLINFO 1\r\n" + + "250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"" + CookieFilePath + "\"\r\n" + + "250-VERSION Tor=\"0.3.3.5\"\r\n" + + "250 OK\r\n" + ) + + expectMsg(ByteString("AUTHCHALLENGE SAFECOOKIE 8969a7f3c03cd21bfd1cc49dbbd8f398345261b5b66319df76bb2fdd8d96bcca\r\n")) + protocolHandler ! ByteString( + "250 AUTHCHALLENGE SERVERHASH=6828e74049924f37cbc61f2aad4dd78d8dc09bef1b4c3bf6ff454016ed9d50df SERVERNONCE=b4aa04b6e7e2df60dcb0f62c264903346e05d1675e77795529e22ca90918dee7\r\n" + ) + + expectMsg(ByteString("AUTHENTICATE 0ddcab5deb39876cdef7af7860a1c738953395349f43b99f4e5e0f131b0515df\r\n")) + protocolHandler ! ByteString( + "250 OK\r\n" + ) + + expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + protocolHandler ! ByteString( + "513 Invalid argument\r\n" + ) + + val t = intercept[TorException] { Await.result(promiseOnionAddress.future, 3 seconds) } + assert(intercept[TorException] { + Await.result(promiseOnionAddress.future, 3 seconds) + } === TorException("server returned error: 513 Invalid argument")) } + } \ No newline at end of file From 1fb003b14f9e2b31144209157ff3c121cedebc0a Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 5 Feb 2019 20:33:18 +0100 Subject: [PATCH 30/40] refactored tor2/tor3 data types and codecs --- .../scala/fr/acinq/eclair/NodeParams.scala | 17 +++-- .../main/scala/fr/acinq/eclair/Setup.scala | 13 ++-- .../fr/acinq/eclair/api/JsonSerializers.scala | 6 +- .../scala/fr/acinq/eclair/db/PeersDb.scala | 7 +- .../eclair/db/sqlite/SqlitePeersDb.scala | 17 ++--- .../fr/acinq/eclair/io/Authenticator.scala | 25 ++++--- .../scala/fr/acinq/eclair/io/Client.scala | 23 +++--- .../main/scala/fr/acinq/eclair/io/Peer.scala | 35 ++++----- .../scala/fr/acinq/eclair/io/Server.scala | 2 +- .../fr/acinq/eclair/io/Switchboard.scala | 8 +-- .../acinq/eclair/router/Announcements.scala | 3 +- .../fr/acinq/eclair/tor/OnionAddress.scala | 68 ------------------ .../acinq/eclair/tor/Socks5Connection.scala | 16 +++-- .../acinq/eclair/tor/TorProtocolHandler.scala | 12 ++-- .../eclair/wire/LightningMessageCodecs.scala | 15 ++-- .../eclair/wire/LightningMessageTypes.scala | 71 +++++++------------ .../scala/fr/acinq/eclair/TestConstants.scala | 6 +- .../acinq/eclair/api/JsonRpcServiceSpec.scala | 2 +- .../eclair/api/JsonSerializersSpec.scala | 7 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 13 ++-- .../acinq/eclair/db/SqlitePeersDbSpec.scala | 9 +-- .../eclair/integration/IntegrationSpec.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 5 +- .../eclair/tor/TorProtocolHandlerSpec.scala | 31 ++++---- .../wire/LightningMessageCodecsSpec.scala | 8 +-- .../gui/controllers/MainController.scala | 8 +-- 26 files changed, 168 insertions(+), 261 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 8a0ac62e45..edbbe2fed5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -24,7 +24,6 @@ import java.util.concurrent.TimeUnit import com.google.common.net.InetAddresses import com.typesafe.config.{Config, ConfigFactory} -import fr.acinq.bitcoin.{BinaryData, Block, Crypto} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, Block} import fr.acinq.eclair.NodeParams.WatcherType @@ -33,7 +32,7 @@ import fr.acinq.eclair.crypto.KeyManager import fr.acinq.eclair.db._ import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.wire.Color +import fr.acinq.eclair.wire.{Color, NodeAddress} import scala.collection.JavaConversions._ import scala.concurrent.duration.FiniteDuration @@ -44,7 +43,7 @@ import scala.concurrent.duration.FiniteDuration case class NodeParams(keyManager: KeyManager, alias: String, color: Color, - publicAddresses: List[InetSocketAddress], + publicAddresses: List[NodeAddress], globalFeatures: BinaryData, localFeatures: BinaryData, overrideFeatures: Map[PublicKey, (BinaryData, BinaryData)], @@ -131,7 +130,7 @@ object NodeParams { } } - def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager, torAddressOpt: Option[InetSocketAddress]): NodeParams = { + def makeNodeParams(datadir: File, config: Config, keyManager: KeyManager, torAddress_opt: Option[NodeAddress]): NodeParams = { datadir.mkdirs() @@ -197,13 +196,17 @@ object NodeParams { None } + val addresses = config.getStringList("server.public-ips") + .toList + .map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))) + .map(NodeAddress(_)) ++ torAddress_opt + + NodeParams( keyManager = keyManager, alias = nodeAlias, color = Color(color.data(0), color.data(1), color.data(2)), - publicAddresses = config.getStringList("server.public-ips").toList - .map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))) - ++ torAddressOpt.map(List(_)).getOrElse(List()), + publicAddresses = addresses, globalFeatures = BinaryData(config.getString("global-features")), localFeatures = BinaryData(config.getString("local-features")), overrideFeatures = overrideFeatures, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index c25c0544b6..d55907501b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -48,7 +48,8 @@ import fr.acinq.eclair.io.{Authenticator, Server, Switchboard} import fr.acinq.eclair.payment._ import fr.acinq.eclair.router._ import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion -import fr.acinq.eclair.tor.{Controller, OnionAddress, TorProtocolHandler} +import fr.acinq.eclair.tor.{Controller, TorProtocolHandler} +import fr.acinq.eclair.wire.NodeAddress import grizzled.slf4j.Logging import org.json4s.JsonAST.JArray @@ -278,7 +279,7 @@ class Setup(datadir: File, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), - publicAddresses = nodeParams.publicAddresses.map(_.getHostString))) + publicAddresses = nodeParams.publicAddresses.map(_.socketAddress.getHostString))) override def appKit: Kit = kit @@ -304,9 +305,9 @@ class Setup(datadir: File, throw e } - private def initTor(): Option[InetSocketAddress] = { + private def initTor(): Option[NodeAddress] = { if (config.getBoolean("tor.enabled")) { - val promiseTorAddress = Promise[OnionAddress]() + val promiseTorAddress = Promise[NodeAddress]() val auth = config.getString("tor.auth") match { case "password" => TorProtocolHandler.Password(config.getString("tor.password")) case "safecookie" => TorProtocolHandler.SafeCookie() @@ -323,8 +324,8 @@ class Setup(datadir: File, protocolHandlerProps = protocolHandlerProps), "tor", SupervisorStrategy.Stop)) val torAddress = await(promiseTorAddress.future, 30 seconds, "tor did not respond after 30 seconds") - logger.info(s"Tor address ${torAddress.toOnion}") - Some(torAddress.toInetSocketAddress) + logger.info(s"Tor address $torAddress") + Some(torAddress) } else { None } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala index cd8ae4b1cf..ce3c12881a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/JsonSerializers.scala @@ -25,7 +25,6 @@ import fr.acinq.eclair.channel.State import fr.acinq.eclair.crypto.ShaChain import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.router.RouteResponse -import fr.acinq.eclair.tor.OnionAddress import fr.acinq.eclair.transactions.Direction import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo} import fr.acinq.eclair.wire._ @@ -124,10 +123,7 @@ class FailureMessageSerializer extends CustomSerializer[FailureMessage](format = })) class NodeAddressSerializer extends CustomSerializer[NodeAddress](format => ({ null},{ - case IPv4(a, p) => JString(HostAndPort.fromParts(a.getHostAddress, p).toString) - case IPv6(a, p) => JString(HostAndPort.fromParts(a.getHostAddress, p).toString) - case Tor2(b, p) => JString(HostAndPort.fromParts(OnionAddress.hostString(b), p).toString) - case Tor3(b, p) => JString(HostAndPort.fromParts(OnionAddress.hostString(b), p).toString) + case n: NodeAddress => JString(HostAndPort.fromParts(n.socketAddress.getHostString, n.socketAddress.getPort).toString) })) class DirectionSerializer extends CustomSerializer[Direction](format => ({ null },{ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index fd11d15c61..2b8e0f0940 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -16,17 +16,16 @@ package fr.acinq.eclair.db -import java.net.InetSocketAddress - import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.wire.NodeAddress trait PeersDb { - def addOrUpdatePeer(nodeId: PublicKey, address: InetSocketAddress) + def addOrUpdatePeer(nodeId: PublicKey, address: NodeAddress) def removePeer(nodeId: PublicKey) - def listPeers(): Map[PublicKey, InetSocketAddress] + def listPeers(): Map[PublicKey, NodeAddress] def close(): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 1b07dd989d..9b3cf3523b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -16,14 +16,12 @@ package fr.acinq.eclair.db.sqlite -import java.net.InetSocketAddress import java.sql.Connection import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.db.PeersDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} -import fr.acinq.eclair.tor.OnionAddress import fr.acinq.eclair.wire._ import scodec.bits.BitVector @@ -37,8 +35,7 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb { statement.executeUpdate("CREATE TABLE IF NOT EXISTS peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") } - override def addOrUpdatePeer(nodeId: Crypto.PublicKey, address: InetSocketAddress): Unit = { - val nodeaddress = NodeAddress(address) + override def addOrUpdatePeer(nodeId: Crypto.PublicKey, nodeaddress: NodeAddress): Unit = { val data = LightningMessageCodecs.nodeaddress.encode(nodeaddress).require.toByteArray using(sqlite.prepareStatement("UPDATE peers SET data=? WHERE node_id=?")) { update => update.setBytes(1, data) @@ -60,19 +57,13 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb { } } - override def listPeers(): Map[PublicKey, InetSocketAddress] = { + override def listPeers(): Map[PublicKey, NodeAddress] = { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT node_id, data FROM peers") - var m: Map[PublicKey, InetSocketAddress] = Map() + var m: Map[PublicKey, NodeAddress] = Map() while (rs.next()) { val nodeid = PublicKey(rs.getBytes("node_id")) - val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value match { - case IPv4(ipv4, port) => new InetSocketAddress(ipv4, port) - case IPv6(ipv6, port) => new InetSocketAddress(ipv6, port) - case Tor2(tor2, port) => OnionAddress.fromParts(tor2, port).toInetSocketAddress - case Tor3(tor3, port) => OnionAddress.fromParts(tor3, port).toInetSocketAddress - case _ => ??? - } + val nodeaddress = LightningMessageCodecs.nodeaddress.decode(BitVector(rs.getBytes("data"))).require.value m += (nodeid -> nodeaddress) } m diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala index e4d1b508ac..c10974a5f8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala @@ -18,15 +18,15 @@ package fr.acinq.eclair.io import java.net.InetSocketAddress -import akka.actor.{Actor, ActorLogging, ActorRef, DiagnosticActorLogging, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated} +import akka.actor.{Actor, ActorRef, DiagnosticActorLogging, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated} import akka.event.Logging.MDC import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.{Logs, NodeParams} import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted import fr.acinq.eclair.io.Authenticator.{Authenticated, AuthenticationFailed, PendingAuth} -import fr.acinq.eclair.wire.LightningMessageCodecs +import fr.acinq.eclair.wire.{LightningMessageCodecs, NodeAddress} +import fr.acinq.eclair.{Logs, NodeParams} /** * The purpose of this class is to serve as a buffer for newly connection before they are authenticated @@ -42,7 +42,7 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo def ready(switchboard: ActorRef, authenticating: Map[ActorRef, PendingAuth]): Receive = { case pending@PendingAuth(connection, remoteNodeId_opt, address, _) => - log.debug(s"authenticating connection to ${address.getHostString}:${address.getPort} (pending=${authenticating.size} handlers=${context.children.size})") + log.debug(s"authenticating connection to ${address.socketAddress.getHostString}:${address.socketAddress.getPort} (pending=${authenticating.size} handlers=${context.children.size})") val transport = context.actorOf(TransportHandler.props( KeyPair(nodeParams.nodeId.toBin, nodeParams.privateKey.toBin), remoteNodeId_opt.map(_.toBin), @@ -55,8 +55,8 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo val pendingAuth = authenticating(transport) import pendingAuth.{address, remoteNodeId_opt} val outgoing = remoteNodeId_opt.isDefined - log.info(s"connection authenticated with $remoteNodeId@${address.getHostString}:${address.getPort} direction=${if (outgoing) "outgoing" else "incoming"}") - switchboard ! Authenticated(connection, transport, remoteNodeId, address, remoteNodeId_opt.isDefined, pendingAuth.origin_opt) + log.info(s"connection authenticated with $remoteNodeId@${address.socketAddress.getHostString}:${address.socketAddress.getPort} direction=${if (outgoing) "outgoing" else "incoming"}") + switchboard ! Authenticated(connection, transport, remoteNodeId, address, pendingAuth.origin_opt) context become ready(switchboard, authenticating - transport) case Terminated(transport) => @@ -88,10 +88,15 @@ object Authenticator { def props(nodeParams: NodeParams): Props = Props(new Authenticator(nodeParams)) // @formatter:off - case class OutgoingConnection(remoteNodeId: PublicKey, address: InetSocketAddress) - case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: InetSocketAddress, origin_opt: Option[ActorRef]) - case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, origin_opt: Option[ActorRef]) - case class AuthenticationFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") + + sealed trait ConnectionDirection { def socketAddress: InetSocketAddress } + case class Incoming(address: InetSocketAddress) extends ConnectionDirection { override def socketAddress = address } + case class Outgoing(nodeAddress: NodeAddress) extends ConnectionDirection { override def socketAddress = nodeAddress.socketAddress } + + case class OutgoingConnection(remoteNodeId: PublicKey, address: NodeAddress) + case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: ConnectionDirection, origin_opt: Option[ActorRef]) + case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, address: ConnectionDirection, origin_opt: Option[ActorRef]) + case class AuthenticationFailed(address: ConnectionDirection) extends RuntimeException(s"connection failed to $address") // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 6ebbf41450..e7c354ef75 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -26,6 +26,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.io.Client.ConnectionFailed import fr.acinq.eclair.tor.Socks5Connection.{Socks5Connect, Socks5Connected} import fr.acinq.eclair.tor.{Socks5Connection, Socks5ProxyParams} +import fr.acinq.eclair.wire.NodeAddress import fr.acinq.eclair.{Logs, NodeParams} import scala.concurrent.duration._ @@ -34,7 +35,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { import context.system @@ -48,8 +49,9 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") proxyAddress case None => - log.info(s"connecting to ${str(remoteAddress)}") - remoteAddress + val peerAddress = remoteAddress.socketAddress + log.info(s"connecting to ${str(peerAddress)}") + peerAddress } IO(Tcp) ! Tcp.Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) context become connecting(addressToConnect) @@ -68,20 +70,21 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine nodeParams.socksProxy_opt match { case Some(proxyParams) => val proxyAddress = peerOrProxyAddress + val peerAddress = remoteAddress.socketAddress log.info(s"connected to proxy ${str(proxyAddress)}") - val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(remoteAddress))) + val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(peerAddress))) context become { case Tcp.CommandFailed(_: Socks5Connect) => - log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") + log.info(s"connection failed to ${str(peerAddress)} via SOCKS5 ${str(proxyAddress)}") origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self case Socks5Connected(_) => - log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") + log.info(s"connected to ${str(peerAddress)} via SOCKS5 proxy ${str(proxyAddress)}") auth(proxy) context become connected(proxy) } case None => - log.info(s"connected to ${str(remoteAddress)}") + log.info(s"connected to ${str(to)}") auth(connection) context become connected(connection) } @@ -103,12 +106,12 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}" - def auth(connection: ActorRef) = authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) + def auth(connection: ActorRef) = authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = Authenticator.Outgoing(remoteAddress), origin_opt = origin_opt) } object Client { - def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) + def props(nodeParams: NodeParams, authenticator: ActorRef, address: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) - case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") + case class ConnectionFailed(address: NodeAddress) extends RuntimeException(s"connection failed to $address") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 301b08bc22..4542c0b500 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -57,14 +57,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(DISCONNECTED) { case Event(Peer.Connect(NodeURI(_, hostAndPort)), d: DisconnectedData) => - val address = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort) - if (d.address_opt == Some(address)) { + val nodeAddress = NodeAddress(new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort)) + if (d.address_opt.contains(nodeAddress)) { // we already know this address, we'll reconnect automatically sender ! "reconnection in progress" stay } else { // we immediately process explicit connection requests to new addresses - context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = Some(sender()))) + context.actorOf(Client.props(nodeParams, authenticator, nodeAddress, remoteNodeId, origin_opt = Some(sender()))) stay } @@ -79,9 +79,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay using d.copy(attempts = d.attempts + 1) } - case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, address, outgoing, origin_opt), d: DisconnectedData) => + case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, address, origin_opt), d: DisconnectedData) => require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1") - log.debug(s"got authenticated connection to $remoteNodeId@${address.getHostString}:${address.getPort}") + log.debug(s"got authenticated connection to $remoteNodeId@${address.socketAddress.getHostString}:${address.socketAddress.getPort}") transport ! TransportHandler.Listener(self) context watch transport val localInit = nodeParams.overrideFeatures.get(remoteNodeId) match { @@ -91,11 +91,14 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor log.info(s"using globalFeatures=${localInit.globalFeatures} and localFeatures=${localInit.localFeatures}") transport ! localInit - // we store the ip upon successful outgoing connection, keeping only the most recent one - if (outgoing) { - nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, address) + val address_opt = address match { + case Authenticator.Outgoing(nodeAddress) => + // we store the ip upon successful outgoing connection, keeping only the most recent one + nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, nodeAddress) + Some(nodeAddress) + case Authenticator.Incoming(_) => None } - goto(INITIALIZING) using InitializingData(if (outgoing) Some(address) else None, transport, d.channels, origin_opt, localInit) + goto(INITIALIZING) using InitializingData(address_opt, transport, d.channels, origin_opt, localInit) case Event(Terminated(actor), d@DisconnectedData(_, channels, _)) if channels.exists(_._2 == actor) => val h = channels.filter(_._2 == actor).keys @@ -139,7 +142,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay } - case Event(Authenticator.Authenticated(connection, _, _, _, _, origin_opt), _) => + case Event(Authenticator.Authenticated(connection, _, _, _, origin_opt), _) => // two connections in parallel origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("there is another connection attempt in progress"))) // we kill this one @@ -503,13 +506,13 @@ object Peer { case class FinalChannelId(id: BinaryData) extends ChannelId sealed trait Data { - def address_opt: Option[InetSocketAddress] + def address_opt: Option[NodeAddress] def channels: Map[_ <: ChannelId, ActorRef] // will be overridden by Map[FinalChannelId, ActorRef] or Map[ChannelId, ActorRef] } case class Nothing() extends Data { override def address_opt = None; override def channels = Map.empty } - case class DisconnectedData(address_opt: Option[InetSocketAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0) extends Data - case class InitializingData(address_opt: Option[InetSocketAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef], localInit: wire.Init) extends Data - case class ConnectedData(address_opt: Option[InetSocketAddress], transport: ActorRef, localInit: wire.Init, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data + case class DisconnectedData(address_opt: Option[NodeAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0) extends Data + case class InitializingData(address_opt: Option[NodeAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef], localInit: wire.Init) extends Data + case class ConnectedData(address_opt: Option[NodeAddress], transport: ActorRef, localInit: wire.Init, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data case class ExpectedPong(ping: Ping, timestamp: Long = Platform.currentTime) case class PingTimeout(ping: Ping) @@ -519,7 +522,7 @@ object Peer { case object INITIALIZING extends State case object CONNECTED extends State - case class Init(previousKnownAddress: Option[InetSocketAddress], storedChannels: Set[HasCommitments]) + case class Init(previousKnownAddress: Option[NodeAddress], storedChannels: Set[HasCommitments]) case class Connect(uri: NodeURI) case object Reconnect case object Disconnect @@ -533,7 +536,7 @@ object Peer { } case object GetPeerInfo case object SendPing - case class PeerInfo(nodeId: PublicKey, state: String, address: Option[InetSocketAddress], channels: Int) + case class PeerInfo(nodeId: PublicKey, state: String, address: Option[NodeAddress], channels: Int) case class PeerRoutingMessage(transport: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala index 3e399b3709..1bec4433b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala @@ -57,7 +57,7 @@ class Server(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke case Connected(remote, _) => log.info(s"connected to $remote") val connection = sender - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = None, address = remote, origin_opt = None) + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = None, address = Authenticator.Incoming(remote), origin_opt = None) listener ! ResumeAccepting(batchSize = 1) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index f5db019c9c..e0347db14e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.io -import java.net.InetSocketAddress - import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.NodeParams @@ -27,7 +25,7 @@ import fr.acinq.eclair.payment.Relayer.RelayPayload import fr.acinq.eclair.payment.{Relayed, Relayer} import fr.acinq.eclair.router.Rebroadcast import fr.acinq.eclair.transactions.{IN, OUT} -import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc} +import fr.acinq.eclair.wire.{NodeAddress, TemporaryNodeFailure, UpdateAddHtlc} import grizzled.slf4j.Logging import scala.util.Success @@ -94,7 +92,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto context become main(peers - remoteNodeId) } - case auth@Authenticator.Authenticated(_, _, remoteNodeId, _, _, _) => + case auth@Authenticator.Authenticated(_, _, remoteNodeId, _, _) => // if this is an incoming connection, we might not yet have created the peer val peer = createOrGetPeer(peers, remoteNodeId, previousKnownAddress = None, offlineChannels = Set.empty) peer forward auth @@ -114,7 +112,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto * @param offlineChannels * @return */ - def createOrGetPeer(peers: Map[PublicKey, ActorRef], remoteNodeId: PublicKey, previousKnownAddress: Option[InetSocketAddress], offlineChannels: Set[HasCommitments]) = { + def createOrGetPeer(peers: Map[PublicKey, ActorRef], remoteNodeId: PublicKey, previousKnownAddress: Option[NodeAddress], offlineChannels: Set[HasCommitments]) = { peers.get(remoteNodeId) match { case Some(peer) => peer case None => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 362de05e53..52c05a36b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -75,9 +75,8 @@ object Announcements { ) } - def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, addresses: List[InetSocketAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { + def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime / 1000): NodeAnnouncement = { require(alias.size <= 32) - val nodeAddresses = addresses.map(NodeAddress(_)) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, "", nodeAddresses) val sig = Crypto.encodeSignature(Crypto.sign(witness, nodeSecret)) :+ 1.toByte NodeAnnouncement( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala deleted file mode 100644 index 85bc570c06..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/OnionAddress.scala +++ /dev/null @@ -1,68 +0,0 @@ -package fr.acinq.eclair.tor - -import java.net.InetSocketAddress - -import org.apache.commons.codec.binary.Base32 - -/** - * Created by rorp - */ -sealed trait OnionAddress { - import OnionAddress._ - - val onionService: String - - def getHostString: String = s"$onionService$OnionSuffix" - - val getPort: Int - - def toOnion: String = s"$getHostString:$getPort" - - def decodedOnionService: Array[Byte] = base32decode(onionService.toUpperCase) - - def toInetSocketAddress: InetSocketAddress = new InetSocketAddress(getHostString, getPort) -} - -case class OnionAddressV2(onionService: String, getPort: Int) extends OnionAddress { - require(onionService.length == OnionAddress.v2Len) -} - -case class OnionAddressV3(onionService: String, getPort: Int) extends OnionAddress { - require(onionService.length == OnionAddress.v3Len) -} - -object OnionAddress { - val OnionSuffix = ".onion" - val v2Len = 16 - val v3Len = 56 - - def hostString(host: Array[Byte]): String = s"${base32encode(host)}$OnionSuffix" - - def fromParts(host: Array[Byte], port: Int): OnionAddress = { - val onionService = base32encode(host) - onionService.length match { - case `v2Len` => OnionAddressV2(onionService, port) - case `v3Len` => OnionAddressV3(onionService, port) - case _ => throw new RuntimeException(s"Invalid Tor address `$onionService`") - } - } - - def fromParts(hostname: String, port: Int): Option[OnionAddress] = if (isOnion(hostname)) { - val onionService = hostname.stripSuffix(OnionSuffix) - onionService.length match { - case `v2Len` => Some(OnionAddressV2(onionService, port)) - case `v3Len` => Some(OnionAddressV3(onionService, port)) - case _ => None - } - } else { - None - } - - def isOnion(hostname: String): Boolean = hostname.endsWith(OnionSuffix) - - def decodeHostname(hostname: String): Array[Byte] = base32decode(hostname.stripSuffix(OnionSuffix)) - - def base32decode(s: String): Array[Byte] = new Base32().decode(s.toUpperCase) - - def base32encode(a: Seq[Byte]): String = new Base32().encodeAsString(a.toArray).toLowerCase -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index c20c9af7bc..701555ef6a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -8,6 +8,7 @@ import akka.util.ByteString import fr.acinq.bitcoin.toHexString import fr.acinq.eclair.randomBytes import fr.acinq.eclair.tor.Socks5Connection.{Credentials, Socks5Connect} +import fr.acinq.eclair.wire._ /** * Simple socks 5 client. It should be given a new connection, and will @@ -128,10 +129,10 @@ class Socks5Connection(connection: ActorRef, credentials_opt: Option[Credentials object Socks5Connection { def props(tcpConnection: ActorRef, credentials_opt: Option[Credentials], command: Socks5Connect): Props = Props(new Socks5Connection(tcpConnection, credentials_opt, command)) - case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event - case class Socks5Connect(address: InetSocketAddress) extends Tcp.Command + case class Socks5Connected(address: InetSocketAddress) extends Tcp.Event + case class Socks5Error(message: String) extends RuntimeException(message) case class Credentials(username: String, password: String) { @@ -205,11 +206,12 @@ case class Socks5ProxyParams(address: InetSocketAddress, credentials_opt: Option object Socks5ProxyParams { - def proxyAddress(remoteAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = - proxyParams.address.getAddress match { - case _ if remoteAddress.getHostString.endsWith(".onion") && proxyParams.useForTor => Some(proxyParams.address) - case _: Inet4Address if proxyParams.useForIPv4 => Some(proxyParams.address) - case _: Inet6Address if proxyParams.useForIPv6 => Some(proxyParams.address) + def proxyAddress(remoteAddress: NodeAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = + remoteAddress match { + case _: IPv4 if proxyParams.useForIPv4 => Some(proxyParams.address) + case _: IPv6 if proxyParams.useForIPv6 => Some(proxyParams.address) + case _: Tor2 if proxyParams.useForTor => Some(proxyParams.address) + case _: Tor3 if proxyParams.useForTor => Some(proxyParams.address) case _ => None } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index 340ff50a14..f9426f7e67 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -1,5 +1,6 @@ package fr.acinq.eclair.tor +import java.net.InetSocketAddress import java.nio.file.attribute.PosixFilePermissions import java.nio.file.{Files, Path, Paths} import java.util @@ -9,6 +10,7 @@ import akka.io.Tcp.Connected import akka.util.ByteString import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.tor.TorProtocolHandler.{Authentication, OnionServiceVersion} +import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -34,14 +36,14 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int], - onionAdded: Option[Promise[OnionAddress]] + onionAdded: Option[Promise[NodeAddress]] ) extends Actor with Stash with ActorLogging { import TorProtocolHandler._ private var receiver: ActorRef = _ - private var address: Option[OnionAddress] = None + private var address: Option[NodeAddress] = None override def receive: Receive = { case Connected(_, _) => @@ -99,8 +101,8 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) address = Some(onionServiceVersion match { - case V2 => OnionAddressV2(serviceId, virtualPort) - case V3 => OnionAddressV3(serviceId, virtualPort) + case V2 => Tor2(serviceId, virtualPort) + case V3 => Tor3(serviceId, virtualPort) }) onionAdded.foreach(_.success(address.get)) log.debug(s"Onion address: ${address.get}") @@ -177,7 +179,7 @@ object TorProtocolHandler { privateKeyPath: Path, virtualPort: Int, targetPorts: Seq[Int] = Seq(), - onionAdded: Option[Promise[OnionAddress]] = None + onionAdded: Option[Promise[NodeAddress]] = None ): Props = Props(new TorProtocolHandler(version, authentication, privateKeyPath, virtualPort, targetPorts, onionAdded)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 830d69e4b4..8bf3984808 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -18,12 +18,14 @@ package fr.acinq.eclair.wire import java.math.BigInteger import java.net.{Inet4Address, Inet6Address, InetAddress} + import com.google.common.cache.{CacheBuilder, CacheLoader} import fr.acinq.bitcoin.Crypto.{Point, PrivateKey, PublicKey, Scalar} import fr.acinq.bitcoin.{BinaryData, Crypto} import fr.acinq.eclair.crypto.{Generators, Sphinx} import fr.acinq.eclair.wire.FixedSizeStrictCodec.bytesStrict import fr.acinq.eclair.{ShortChannelId, UInt64, wire} +import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ import scodec.{Attempt, Codec, DecodeResult, Err, SizeBound} @@ -58,15 +60,16 @@ object LightningMessageCodecs { def ipv6address: Codec[Inet6Address] = bytes(16).exmap(b => attemptFromTry(Inet6Address.getByAddress(null, b.toArray, null)), a => attemptFromTry(ByteVector(a.getAddress))) + def base32(size: Int): Codec[String] = bytes(size).xmap(b => new Base32().encodeAsString(b.toArray).toLowerCase, a => ByteVector(new Base32().decode(a.toUpperCase()))) + def nodeaddress: Codec[NodeAddress] = discriminated[NodeAddress].by(uint8) - .typecase(0, provide(Padding)) - .typecase(1, (ipv4address ~ uint16).xmap[IPv4](x => IPv4(x._1, x._2), x => (x.ipv4, x.port))) - .typecase(2, (ipv6address ~ uint16).xmap[IPv6](x => IPv6(x._1, x._2), x => (x.ipv6, x.port))) - .typecase(3, (binarydata(10) ~ uint16).xmap[Tor2](x => Tor2(x._1, x._2), x => (x.tor2, x.port))) - .typecase(4, (binarydata(35) ~ uint16).xmap[Tor3](x => Tor3(x._1, x._2), x => (x.tor3, x.port))) + .typecase(1, (ipv4address :: uint16).as[IPv4]) + .typecase(2, (ipv6address :: uint16).as[IPv6]) + .typecase(3, (base32(10) :: uint16).as[Tor2]) + .typecase(4, (base32(35) :: uint16).as[Tor3]) - // this one is a bit different from most other codecs: the first 'len' element is * not * the number of items + // this one is a bit different from most other codecs: the first 'len' element is *not* the number of items // in the list but rather the number of bytes of the encoded list. The rationale is once we've read this // number of bytes we can just skip to the next field def listofnodeaddresses: Codec[List[NodeAddress]] = variableSizeBytes(uint16, list(nodeaddress)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 0a907c01b6..3bc2b58054 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -20,9 +20,7 @@ import java.net.{Inet4Address, Inet6Address, InetSocketAddress} import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} -import fr.acinq.eclair.tor.{OnionAddress, OnionAddressV2, OnionAddressV3} import fr.acinq.eclair.{ShortChannelId, UInt64} -import scodec.bits.BitVector /** * Created by PM on 15/11/2016. @@ -162,52 +160,38 @@ case class Color(r: Byte, g: Byte, b: Byte) { } // @formatter:off -sealed trait NodeAddress { - def getHostString: String - def getPort: Int -} -case object NodeAddress { - def apply(inetSocketAddress: InetSocketAddress): NodeAddress = - OnionAddress.fromParts(inetSocketAddress.getHostString, inetSocketAddress.getPort) match { - case Some(OnionAddressV2(onionService, port)) => Tor2(BinaryData(OnionAddress.decodeHostname(onionService)), port) - case Some(OnionAddressV3(onionService, port)) => Tor3(BinaryData(OnionAddress.decodeHostname(onionService)), port) - case _ => - inetSocketAddress.getAddress match { - case a: Inet4Address => IPv4(a, inetSocketAddress.getPort) - case a: Inet6Address => IPv6(a, inetSocketAddress.getPort) - case _ => throw new RuntimeException(s"Invalid socket address $inetSocketAddress") - } - } - def apply(onionAddress: OnionAddress): NodeAddress = { - onionAddress match { - case _: OnionAddressV2 => Tor2(BinaryData(onionAddress.decodedOnionService), onionAddress.getPort) - case _: OnionAddressV3 => Tor3(BinaryData(onionAddress.decodedOnionService), onionAddress.getPort) +sealed trait NodeAddress { def socketAddress: InetSocketAddress } +sealed trait OnionAddress extends NodeAddress +object NodeAddress { + def apply(socketAddress: InetSocketAddress): NodeAddress = { + socketAddress match { + case _ if socketAddress.getHostString.endsWith(".onion") && socketAddress.getHostString.length == 22 => Tor2(socketAddress.getHostString.dropRight(6), socketAddress.getPort) + case _ if socketAddress.getHostString.endsWith(".onion") && socketAddress.getHostString.length == 62 => Tor3(socketAddress.getHostString.dropRight(6), socketAddress.getPort) + case _ => socketAddress.getAddress match { + case a: Inet4Address => IPv4(a, socketAddress.getPort) + case a: Inet6Address => IPv6(a, socketAddress.getPort) + case _ => throw new IllegalArgumentException(s"cannot convert $socketAddress to node address") + } } } } -case object Padding extends NodeAddress { - override def getHostString: String = "" - override def getPort: Int = 0 -} +// @formatter:on case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { - override def getHostString: String = ipv4.getHostAddress - override def getPort: Int = port + override def socketAddress = new InetSocketAddress(ipv4, port) } + case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { - override def getHostString: String = ipv6.getHostAddress - override def getPort: Int = port + override def socketAddress = new InetSocketAddress(ipv6, port) } -case class Tor2(tor2: BinaryData, port: Int) extends NodeAddress { - require(tor2.size == 10) - override def getHostString: String = OnionAddress.hostString(tor2) - override def getPort: Int = port + +case class Tor2(tor2: String, port: Int) extends OnionAddress { + override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) } -case class Tor3(tor3: BinaryData, port: Int) extends NodeAddress { - require(tor3.size == 35) - override def getHostString: String = OnionAddress.hostString(tor3) - override def getPort: Int = port + +case class Tor3(tor3: String, port: Int) extends OnionAddress { + override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) } -// @formatter:on + case class NodeAnnouncement(signature: BinaryData, features: BinaryData, @@ -215,14 +199,7 @@ case class NodeAnnouncement(signature: BinaryData, nodeId: PublicKey, rgbColor: Color, alias: String, - addresses: List[NodeAddress]) extends RoutingMessage with HasTimestamp { - def socketAddresses: List[InetSocketAddress] = addresses.collect { - case IPv4(a, port) => new InetSocketAddress(a, port) - case IPv6(a, port) => new InetSocketAddress(a, port) - case Tor2(a, port) => OnionAddress.fromParts(a, port).toInetSocketAddress - case Tor3(a, port) => OnionAddress.fromParts(a, port).toInetSocketAddress - } -} + addresses: List[NodeAddress]) extends RoutingMessage with HasTimestamp case class ChannelUpdate(signature: BinaryData, chainHash: BinaryData, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 4d45bda5e2..1bb1b83a2f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer -import fr.acinq.eclair.wire.Color +import fr.acinq.eclair.wire.{Color, NodeAddress} import scala.concurrent.duration._ @@ -48,7 +48,7 @@ object TestConstants { keyManager = keyManager, alias = "alice", color = Color(1, 2, 3), - publicAddresses = new InetSocketAddress("localhost", 9731) :: Nil, + publicAddresses = NodeAddress(new InetSocketAddress("localhost", 9731)) :: Nil, globalFeatures = "", localFeatures = "00", overrideFeatures = Map.empty, @@ -108,7 +108,7 @@ object TestConstants { keyManager = keyManager, alias = "bob", color = Color(4, 5, 6), - publicAddresses = new InetSocketAddress("localhost", 9732) :: Nil, + publicAddresses = NodeAddress(new InetSocketAddress("localhost", 9732)) :: Nil, globalFeatures = "", localFeatures = "00", // no announcement overrideFeatures = Map.empty, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala index fccad89dfc..4b56929b91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala @@ -196,7 +196,7 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { port = 9735, chainHash = Alice.nodeParams.chainHash, blockHeight = 123456, - publicAddresses = Alice.nodeParams.publicAddresses.map(_.getHostString) + publicAddresses = Alice.nodeParams.publicAddresses.map(_.socketAddress.getHostString) )) } import mockService.formats diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 65e383b0a5..7e9e106a83 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -19,10 +19,9 @@ package fr.acinq.eclair.api import java.net.{InetAddress, InetSocketAddress} import fr.acinq.bitcoin.{BinaryData, OutPoint} -import fr.acinq.eclair.tor.OnionAddress import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.transactions.{IN, OUT} -import fr.acinq.eclair.wire.NodeAddress +import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import org.json4s.jackson.Serialization import org.scalatest.{FunSuite, Matchers} @@ -53,8 +52,8 @@ class JsonSerializersSpec extends FunSuite with Matchers { test("NodeAddress serialization") { val ipv4 = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(10, 0, 0, 1)), 8888)) val ipv6LocalHost = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735)) - val tor2 = NodeAddress(OnionAddress.fromParts(Array.tabulate(10)(_.toByte), 7777).toInetSocketAddress) - val tor3 = NodeAddress(OnionAddress.fromParts(Array.tabulate(35)(_.toByte), 9999).toInetSocketAddress) + val tor2 = Tor2("aaaqeayeaudaocaj", 7777) + val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) Serialization.write(ipv4)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" Serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + new NodeAddressSerializer) shouldBe s""""[0:0:0:0:0:0:0:1]:9735"""" diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index f9080e0568..9ef92e4f89 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -22,8 +22,7 @@ import java.sql.DriverManager import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements -import fr.acinq.eclair.tor.OnionAddressV2 -import fr.acinq.eclair.wire.Color +import fr.acinq.eclair.wire.{Color, NodeAddress, Tor2} import fr.acinq.eclair.{ShortChannelId, randomKey} import org.scalatest.FunSuite import org.sqlite.SQLiteException @@ -43,10 +42,10 @@ class SqliteNetworkDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000) :: Nil) - val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), OnionAddressV2("aaaqeayeaudaocaj", 42000).toInetSocketAddress :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil) assert(db.listNodes().toSet === Set.empty) db.addNode(node_1) @@ -60,7 +59,7 @@ class SqliteNetworkDbSpec extends FunSuite { assert(db.listNodes().toSet === Set(node_1, node_3, node_4)) db.updateNode(node_1) - assert(node_4.socketAddresses == List(new InetSocketAddress("aaaqeayeaudaocaj.onion", 42000))) + assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) } test("add/remove/list channels and channel_updates") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala index 09461f6a2c..3256ef365e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala @@ -21,6 +21,7 @@ import java.sql.DriverManager import fr.acinq.eclair.db.sqlite.SqlitePeersDb import fr.acinq.eclair.randomKey +import fr.acinq.eclair.wire.NodeAddress import org.scalatest.FunSuite @@ -38,10 +39,10 @@ class SqlitePeersDbSpec extends FunSuite { val sqlite = inmem val db = new SqlitePeersDb(sqlite) - val peer_1 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 1111)) - val peer_1_bis = (peer_1._1, new InetSocketAddress(InetAddress.getLoopbackAddress, 1112)) - val peer_2 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 2222)) - val peer_3 = (randomKey.publicKey, new InetSocketAddress(InetAddress.getLoopbackAddress, 3333)) + val peer_1 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 1111))) + val peer_1_bis = (peer_1._1, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 1112))) + val peer_2 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 2222))) + val peer_3 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 3333))) assert(db.listPeers().toSet === Set.empty) db.addOrUpdatePeer(peer_1._1, peer_1._2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index b97fee1aa4..ccd2bf22d9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -131,7 +131,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val address = node2.nodeParams.publicAddresses.head sender.send(node1.switchboard, Peer.Connect(NodeURI( nodeId = node2.nodeParams.nodeId, - address = HostAndPort.fromParts(address.getHostString, address.getPort)))) + address = HostAndPort.fromParts(address.socketAddress.getHostString, address.socketAddress.getPort)))) sender.expectMsgAnyOf(10 seconds, "connected", "already connected") sender.send(node1.switchboard, Peer.OpenChannel( remoteNodeId = node2.nodeParams.nodeId, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 21ae51b30f..b30338b0ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -9,10 +9,11 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler +import fr.acinq.eclair.io.Authenticator.Outgoing import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} -import fr.acinq.eclair.wire.{Error, Ping, Pong} +import fr.acinq.eclair.wire.{Error, NodeAddress, Ping, Pong} import fr.acinq.eclair.{ShortChannelId, TestkitBaseClass, wire} import org.scalatest.Outcome @@ -45,7 +46,7 @@ class PeerSpec extends TestkitBaseClass { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, Set.empty)) - authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, InetSocketAddress.createUnresolved("foo.bar", 42000), false, None)) + authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(NodeAddress(new InetSocketAddress("1.2.3.4", 42000))), None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index bdef62dd59..6a7638dffa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -9,7 +9,7 @@ import akka.testkit.{ImplicitSender, TestActorRef, TestKit} import akka.util.ByteString import fr.acinq.bitcoin.BinaryData import fr.acinq.eclair.TestUtils -import fr.acinq.eclair.wire.NodeAddress +import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import org.scalatest._ import scala.concurrent.duration._ @@ -36,7 +36,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) } ignore("connect to real tor daemon") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() val protocolHandlerProps = TorProtocolHandler.props( version = OnionServiceVersion("v2"), @@ -49,13 +49,10 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) val address = Await.result(promiseOnionAddress.future, 30 seconds) println(address) - println(address.onionService.length) - println((address.onionService + ".onion").length) - println(NodeAddress(address)) } test("happy path v2") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( version = OnionServiceVersion("v2"), @@ -86,18 +83,16 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) "250 OK\r\n" ) protocolHandler ! GetOnionAddress - expectMsg(Some(OnionAddressV2("z4zif3fy7fe7bpg3", 9999))) + expectMsg(Some(Tor2("z4zif3fy7fe7bpg3", 9999))) val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === OnionAddressV2("z4zif3fy7fe7bpg3", 9999)) - assert(address.toOnion === "z4zif3fy7fe7bpg3.onion:9999") - assert(NodeAddress(address).toString === "Tor2(cf3282ecb8f949f0bcdb,9999)") + assert(address === Tor2("z4zif3fy7fe7bpg3", 9999)) assert(readString(PkFilePath) === "RSA1024:private-key") } test("happy path v3") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( version = OnionServiceVersion("v3"), @@ -129,12 +124,10 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) ) protocolHandler ! GetOnionAddress - expectMsg(Some(OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) + expectMsg(Some(Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999))) val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === OnionAddressV3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) - assert(address.toOnion === "mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd.onion:9999") - assert(NodeAddress(address).toString === "Tor3(6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03,9999)") + assert(address === Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 9999)) assert(readString(PkFilePath) === "ED25519-V3:private-key") } @@ -150,7 +143,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) } test("authentication method errors") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( version = OnionServiceVersion("v2"), @@ -175,7 +168,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) } test("invalid server hash") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() Files.write(CookieFilePath, fr.acinq.eclair.randomBytes(32)) @@ -208,7 +201,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) test("AUTHENTICATE failure") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() Files.write(CookieFilePath, BinaryData(AuthCookie)) @@ -245,7 +238,7 @@ class TorProtocolHandlerSpec extends TestKit(ActorSystem("test")) } test("ADD_ONION failure") { - val promiseOnionAddress = Promise[OnionAddress]() + val promiseOnionAddress = Promise[NodeAddress]() Files.write(CookieFilePath, BinaryData(AuthCookie)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index efd7188ea5..85865b0a7d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -102,16 +102,16 @@ class LightningMessageCodecsSpec extends FunSuite { assert(nodeaddr === nodeaddr2) } { - val nodeaddr = Tor2(Array.tabulate(10)(_.toByte), 4231) + val nodeaddr = Tor2("z4zif3fy7fe7bpg3", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"03 00 01 02 03 04 05 06 07 08 09 10 87".toBitVector) + assert(bin === hex"03 cf3282ecb8f949f0bcdb 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value assert(nodeaddr === nodeaddr2) } { - val nodeaddr = Tor3(Array.tabulate(35)(_.toByte), 4231) + val nodeaddr = Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231) val bin = nodeaddress.encode(nodeaddr).require - assert(bin === hex"04 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 10 87".toBitVector) + assert(bin === hex"04 6457a1ed0b38a73d56dc866accec93ca6af68bc316568874478dc9399cc1a0b3431b03 1087".toBitVector) val nodeaddr2 = nodeaddress.decode(bin).require.value assert(nodeaddr === nodeaddr2) } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index 81a8a77b85..827149a2d9 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -195,7 +195,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext }) networkNodesIPColumn.setCellValueFactory(new Callback[CellDataFeatures[NodeAnnouncement, String], ObservableValue[String]]() { def call(pn: CellDataFeatures[NodeAnnouncement, String]) = { - val address = pn.getValue.socketAddresses.map(a => HostAndPort.fromParts(a.getHostString, a.getPort)).mkString(",") + val address = pn.getValue.addresses.map(a => HostAndPort.fromParts(a.socketAddress.getHostString, a.socketAddress.getPort)).mkString(",") new SimpleStringProperty(address) } }) @@ -365,7 +365,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext bitcoinChain.getStyleClass.add(setup.chain) val nodeURI_opt = setup.nodeParams.publicAddresses.headOption.map(address => { - s"${setup.nodeParams.nodeId}@${HostAndPort.fromParts(address.getHostString, address.getPort)}" + s"${setup.nodeParams.nodeId}@${HostAndPort.fromParts(address.socketAddress.getHostString, address.socketAddress.getPort)}" }) contextMenu = ContextMenuUtils.buildCopyContext(List(CopyAction("Copy Pubkey", setup.nodeParams.nodeId.toString()))) @@ -456,8 +456,8 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext copyURI.setOnAction(new EventHandler[ActionEvent] { override def handle(event: ActionEvent): Unit = Option(row.getItem) match { case Some(pn) => ContextMenuUtils.copyToClipboard( - pn.socketAddresses.headOption match { - case Some(firstAddress) => s"${pn.nodeId.toString}@${HostAndPort.fromParts(firstAddress.getHostString, firstAddress.getPort)}" + pn.addresses.headOption match { + case Some(firstAddress) => s"${pn.nodeId.toString}@${HostAndPort.fromParts(firstAddress.socketAddress.getHostString, firstAddress.socketAddress.getPort)}" case None => "no URI Known" }) case None => From 769c31832a1990c7cf56967589d5edf928b365e1 Mon Sep 17 00:00:00 2001 From: pm47 Date: Tue, 5 Feb 2019 20:42:03 +0100 Subject: [PATCH 31/40] added missing copyrights --- .../electrum/ElectrumClientPool.scala | 18 +++++++++--------- .../blockchain/electrum/db/WalletDb.scala | 16 ++++++++++++++++ .../electrum/db/sqlite/SqliteWalletDb.scala | 16 ++++++++++++++++ .../blockchain/fee/SmoothFeeProvider.scala | 16 ++++++++++++++++ .../fr/acinq/eclair/payment/Autoprobe.scala | 16 ++++++++++++++++ .../eclair/router/ChannelRangeQueries.scala | 16 ++++++++++++++++ .../scala/fr/acinq/eclair/router/Graph.scala | 16 ++++++++++++++++ .../scala/fr/acinq/eclair/tor/Controller.scala | 16 ++++++++++++++++ .../fr/acinq/eclair/tor/Socks5Connection.scala | 16 ++++++++++++++++ .../acinq/eclair/tor/TorProtocolHandler.scala | 16 ++++++++++++++++ .../scala/fr/acinq/eclair/StartupSpec.scala | 16 ++++++++++++++++ .../test/scala/fr/acinq/eclair/TestUtils.scala | 16 ++++++++++++++++ .../acinq/eclair/api/JsonRpcServiceSpec.scala | 16 ++++++++++++++++ .../electrum/ElectrumClientPoolSpec.scala | 18 +++++++++--------- .../db/sqlite/SqliteWalletDbSpec.scala | 16 ++++++++++++++++ .../fee/BitcoinCoreFeeProviderSpec.scala | 16 ++++++++++++++++ .../blockchain/fee/SmoothFeeProviderSpec.scala | 16 ++++++++++++++++ .../scala/fr/acinq/eclair/io/PeerSpec.scala | 16 ++++++++++++++++ .../eclair/payment/ChannelSelectionSpec.scala | 16 ++++++++++++++++ .../router/ChannelRangeQueriesSpec.scala | 16 ++++++++++++++++ .../fr/acinq/eclair/router/GraphSpec.scala | 16 ++++++++++++++++ .../acinq/eclair/router/RoutingSyncSpec.scala | 16 ++++++++++++++++ .../eclair/tor/TorProtocolHandlerSpec.scala | 16 ++++++++++++++++ 23 files changed, 354 insertions(+), 18 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala index 0401c48726..15956a2670 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala @@ -1,17 +1,17 @@ /* * Copyright 2018 ACINQ SAS * - * 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 + * 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 + * 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. + * 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. */ package fr.acinq.eclair.blockchain.electrum diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala index 6ddb6e57c7..00b0d26eb4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.electrum.db import fr.acinq.bitcoin.{BinaryData, BlockHeader, Transaction} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala index 00181396e1..70e3188a6a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.electrum.db.sqlite import java.sql.Connection diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala index 5fd1520516..9e68e82727 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.fee import scala.concurrent.{ExecutionContext, Future} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index b446884132..882cbaab6b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, ActorRef, Props} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala index 4b9d87625d..46ac4a743d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/ChannelRangeQueries.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.router import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala index 88aacd172f..37c9f97aa6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Graph.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.PublicKey diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala index b0cf6c9f0b..484e9524df 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Controller.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.tor import java.net.InetSocketAddress diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 701555ef6a..e3c74c42e0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.tor import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala index f9426f7e67..fc117952c4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/TorProtocolHandler.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.tor import java.net.InetSocketAddress diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala index cb0d7c9d8d..de3e3b39aa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair import java.io.{File, IOException} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala index acd772b83c..3a3d32aa8c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestUtils.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair import java.io.File diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala index 4b56929b91..9a95473af8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.api diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala index 082033e9bb..d383d86a8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala @@ -1,17 +1,17 @@ /* * Copyright 2018 ACINQ SAS * - * 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 + * 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 + * 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. + * 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. */ package fr.acinq.eclair.blockchain.electrum diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala index 5f5c5a3b0c..196ebcab36 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.electrum.db.sqlite import java.sql.DriverManager diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala index 493a835e78..82c292166c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitcoinCoreFeeProviderSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.fee import akka.actor.ActorSystem diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala index 882bd3da84..efa22b631c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProviderSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.blockchain.fee import org.scalatest.FunSuite diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index b30338b0ac..5c8ce09c74 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.io import java.net.InetSocketAddress diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index b56ff6be09..6fe5be331c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.payment import fr.acinq.bitcoin.Block diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala index 4e88450720..42fdd35ceb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/ChannelRangeQueriesSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.router import fr.acinq.bitcoin.Block diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala index daacfba5f6..5d819a3368 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/GraphSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.PublicKey diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala index ff5c861dca..4a2434b569 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RoutingSyncSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.router import akka.actor.ActorSystem diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala index 6a7638dffa..085e1f3b25 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/TorProtocolHandlerSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + package fr.acinq.eclair.tor import java.net.InetSocketAddress From 0592cdecf95343f596e0477c349583c4b7d94b01 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 6 Feb 2019 13:39:06 +0100 Subject: [PATCH 32/40] updated tor documentation --- TOR.md | 51 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/TOR.md b/TOR.md index 6ad663d558..edb1558135 100644 --- a/TOR.md +++ b/TOR.md @@ -2,68 +2,81 @@ ### Installing Tor on your node -For Linux: +#### Linux: ```shell sudo apt install tor ``` -For Mac OS X: +#### Mac OS X: ```shell brew install tor ``` -For Windows: +#### Windows: -Download the "Expert Bundle" from https://www.torproject.org/download/download.html and extract it to the root of your drive (e.g. `C:\tor`). +[Download the "Expert Bundle"](https://www.torproject.org/download/download.html) from Tor's website and extract it to `C:\tor`. ### Configuring Tor +#### Linux and Max OS X: + +Eclair requires safe cookie authentication as well as SOCKS5 and control connections to be enabled. + +Edit Tor configuration file `/etc/tor/torrc` (Linux) or `/usr/local/etc/tor/torrc` (Mac OS X). + +``` +SOCKSPort 9050 +ControlPort 9051 +CookieAuthentication 1 +ExitPolicy reject *:* # don't change this unless you really know what you are doing +``` + +Make sure eclair is allowed to read Tor's cookie file (typically `/var/run/tor/control.authcookie`). + +#### Windows: + +On Windows it is easier to use the password authentication mechanism. + First pick a password and hash it with this command: ```shell +$ cd c:\tor\Tor $ tor --hash-password this-is-an-example-password-change-it 16:94A50709CAA98333602756426F43E6AC6760B9ADEF217F58219E639E5A ``` -Edit Tor configuration file: - - `/etc/tor/torrc` (Linux) - - `/usr/local/etc/tor/torrc` (Mac OS X) - - `C:\tor\conf\torrc` (Windows) - -Replace the value for `HashedControlPassword` with the result of the command above. +Create a Tor configuration file (`C:\tor\Conf\torrc`), edit it and replace the value for `HashedControlPassword` with the result of the command above. ``` SOCKSPort 9050 ControlPort 9051 -HashedControlPassword 16:--REPLACE--THIS--WITH--THE--HASH--OF--YOUR--PASSWORD -ExitPolicy reject *:* +HashedControlPassword 16:--REPLACE--THIS--WITH--THE--HASH--OF--YOUR--PASSWORD-- +ExitPolicy reject *:* # don't change this unless you really know what you are doing ``` -Eclair requires password authentication as well as SOCKS5 and control connections to be enabled. -Change the value of the `ExitPolicy` parameter only if you really know what you are doing. - ### Start Tor -For Linux: +#### Linux: ```shell sudo systemctl start tor ``` -For Mac OS X: +#### Mac OS X: ```shell brew services start tor ``` -For Windows: +#### Windows: Open a CMD with administrator access ```shell -tor --service install +cd c:\tor\Tor +tor --service install -options -f c:\tor\Conf\torrc ``` ### Configure Tor hidden service From d2e29648941a5291eb3479a3b1d105c73acaf684 Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 6 Feb 2019 15:11:39 +0100 Subject: [PATCH 33/40] reworked NodeAddress constructor --- .../scala/fr/acinq/eclair/NodeParams.scala | 7 ++--- .../main/scala/fr/acinq/eclair/io/Peer.scala | 23 +++++++------- .../eclair/wire/LightningMessageTypes.scala | 31 +++++++++++++------ .../scala/fr/acinq/eclair/TestConstants.scala | 6 ++-- .../eclair/api/JsonSerializersSpec.scala | 5 +-- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 7 +++-- .../acinq/eclair/db/SqlitePeersDbSpec.scala | 11 ++++--- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 3 +- 8 files changed, 55 insertions(+), 38 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 1e27c90765..45f75ae7d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -22,7 +22,7 @@ import java.nio.file.Files import java.sql.DriverManager import java.util.concurrent.TimeUnit -import com.google.common.net.InetAddresses +import com.google.common.net.{HostAndPort, InetAddresses} import com.typesafe.config.{Config, ConfigFactory} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{BinaryData, Block} @@ -199,9 +199,8 @@ object NodeParams { val addresses = config.getStringList("server.public-ips") .toList - .map(ip => new InetSocketAddress(InetAddresses.forString(ip), config.getInt("server.port"))) - .map(NodeAddress(_)) ++ torAddress_opt - + .map(ip => HostAndPort.fromParts(InetAddresses.forString(ip).getHostAddress, config.getInt("server.port"))) + .map(NodeAddress.from(_).get) ++ torAddress_opt NodeParams( keyManager = keyManager, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 5e2db44264..4afa65b0d0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -34,7 +34,7 @@ import scodec.Attempt import scala.compat.Platform import scala.concurrent.duration._ -import scala.util.Random +import scala.util.{Failure, Random, Success} /** * Created by PM on 26/08/2016. @@ -57,16 +57,17 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(DISCONNECTED) { case Event(Peer.Connect(NodeURI(_, hostAndPort)), d: DisconnectedData) => - val nodeAddress = NodeAddress(new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort)) - if (d.address_opt.contains(nodeAddress)) { - // we already know this address, we'll reconnect automatically - sender ! "reconnection in progress" - stay - } else { - // we immediately process explicit connection requests to new addresses - context.actorOf(Client.props(nodeParams, authenticator, nodeAddress, remoteNodeId, origin_opt = Some(sender()))) - stay + NodeAddress.from(hostAndPort) match { + case Failure(_) => + sender ! "couldn't resolve address" + case Success(nodeAddress) if d.address_opt.contains(nodeAddress) => + // we already know this address, we'll reconnect automatically + sender ! "reconnection in progress" + case Success(nodeAddress) => + // we immediately process explicit connection requests to new addresses + context.actorOf(Client.props(nodeParams, authenticator, nodeAddress, remoteNodeId, origin_opt = Some(sender()))) } + stay case Event(Reconnect, d: DisconnectedData) => d.address_opt match { @@ -134,7 +135,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor // let's bring existing/requested channels online d.channels.values.toSet[ActorRef].foreach(_ ! INPUT_RECONNECTED(d.transport, d.localInit, remoteInit)) // we deduplicate with toSet because there might be two entries per channel (tmp id and final id) - goto(CONNECTED) using ConnectedData(d.address_opt, d.transport, d.localInit, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) }) forMax(30 seconds) // forMax will trigger a StateTimeout + goto(CONNECTED) using ConnectedData(d.address_opt, d.transport, d.localInit, remoteInit, d.channels.map { case (k: ChannelId, v) => (k, v) }) forMax (30 seconds) // forMax will trigger a StateTimeout } else { log.warning(s"incompatible features, disconnecting") d.origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("incompatible features"))) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 3bc2b58054..c738f127e2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -16,12 +16,15 @@ package fr.acinq.eclair.wire -import java.net.{Inet4Address, Inet6Address, InetSocketAddress} +import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} +import com.google.common.net.HostAndPort import fr.acinq.bitcoin.BinaryData import fr.acinq.bitcoin.Crypto.{Point, PublicKey, Scalar} import fr.acinq.eclair.{ShortChannelId, UInt64} +import scala.util.{Success, Try} + /** * Created by PM on 15/11/2016. */ @@ -163,14 +166,24 @@ case class Color(r: Byte, g: Byte, b: Byte) { sealed trait NodeAddress { def socketAddress: InetSocketAddress } sealed trait OnionAddress extends NodeAddress object NodeAddress { - def apply(socketAddress: InetSocketAddress): NodeAddress = { - socketAddress match { - case _ if socketAddress.getHostString.endsWith(".onion") && socketAddress.getHostString.length == 22 => Tor2(socketAddress.getHostString.dropRight(6), socketAddress.getPort) - case _ if socketAddress.getHostString.endsWith(".onion") && socketAddress.getHostString.length == 62 => Tor3(socketAddress.getHostString.dropRight(6), socketAddress.getPort) - case _ => socketAddress.getAddress match { - case a: Inet4Address => IPv4(a, socketAddress.getPort) - case a: Inet6Address => IPv6(a, socketAddress.getPort) - case _ => throw new IllegalArgumentException(s"cannot convert $socketAddress to node address") + /** + * Creates a NodeAddress from a HostPort. + * + * Note that non-onion hosts will be resolved. + * + * We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on + * the .onion TLD and rely on their length to separate v2/v3. + * + * @param hostPort + * @return + */ + def from(hostPort: HostAndPort): Try[NodeAddress] = Try { + hostPort match { + case _ if hostPort.getHost.endsWith(".onion") && hostPort.getHost.length == 22 => Tor2(hostPort.getHost.dropRight(6), hostPort.getPort) + case _ if hostPort.getHost.endsWith(".onion") && hostPort.getHost.length == 62 => Tor3(hostPort.getHost.dropRight(6), hostPort.getPort) + case _ => InetAddress.getByName(hostPort.getHost) match { + case a: Inet4Address => IPv4(a, hostPort.getPort) + case a: Inet6Address => IPv6(a, hostPort.getPort) } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 796b81cbec..0a2878624f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -16,9 +16,9 @@ package fr.acinq.eclair -import java.net.InetSocketAddress import java.sql.DriverManager +import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{BinaryData, Block, Script} import fr.acinq.eclair.NodeParams.BITCOIND @@ -48,7 +48,7 @@ object TestConstants { keyManager = keyManager, alias = "alice", color = Color(1, 2, 3), - publicAddresses = NodeAddress(new InetSocketAddress("localhost", 9731)) :: Nil, + publicAddresses = NodeAddress.from(HostAndPort.fromParts("localhost", 9731)).get :: Nil, globalFeatures = "", localFeatures = "00", overrideFeatures = Map.empty, @@ -109,7 +109,7 @@ object TestConstants { keyManager = keyManager, alias = "bob", color = Color(4, 5, 6), - publicAddresses = NodeAddress(new InetSocketAddress("localhost", 9732)) :: Nil, + publicAddresses = NodeAddress.from(HostAndPort.fromParts("localhost", 9732)).get :: Nil, globalFeatures = "", localFeatures = "00", // no announcement overrideFeatures = Map.empty, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 7e9e106a83..99c0564b63 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.api import java.net.{InetAddress, InetSocketAddress} +import com.google.common.net.HostAndPort import fr.acinq.bitcoin.{BinaryData, OutPoint} import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.transactions.{IN, OUT} @@ -50,8 +51,8 @@ class JsonSerializersSpec extends FunSuite with Matchers { } test("NodeAddress serialization") { - val ipv4 = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(10, 0, 0, 1)), 8888)) - val ipv6LocalHost = NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735)) + val ipv4 = NodeAddress.from(HostAndPort.fromParts("10.0.0.1", 8888)).get + val ipv6LocalHost = NodeAddress.from(HostAndPort.fromParts(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)).getHostAddress, 9735)).get val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 9ef92e4f89..98d4dc4292 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.db import java.net.{InetAddress, InetSocketAddress} import java.sql.DriverManager +import com.google.common.net.HostAndPort import fr.acinq.bitcoin.{Block, Crypto, Satoshi} import fr.acinq.eclair.db.sqlite.SqliteNetworkDb import fr.acinq.eclair.router.Announcements @@ -42,9 +43,9 @@ class SqliteNetworkDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress(new InetSocketAddress(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)), 42000)) :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil) assert(db.listNodes().toSet === Set.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala index 3256ef365e..64db06e7c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala @@ -19,9 +19,10 @@ package fr.acinq.eclair.db import java.net.{InetAddress, InetSocketAddress} import java.sql.DriverManager +import com.google.common.net.HostAndPort import fr.acinq.eclair.db.sqlite.SqlitePeersDb import fr.acinq.eclair.randomKey -import fr.acinq.eclair.wire.NodeAddress +import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3} import org.scalatest.FunSuite @@ -39,10 +40,10 @@ class SqlitePeersDbSpec extends FunSuite { val sqlite = inmem val db = new SqlitePeersDb(sqlite) - val peer_1 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 1111))) - val peer_1_bis = (peer_1._1, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 1112))) - val peer_2 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 2222))) - val peer_3 = (randomKey.publicKey, NodeAddress(new InetSocketAddress(InetAddress.getLoopbackAddress, 3333))) + val peer_1 = (randomKey.publicKey, NodeAddress.from(HostAndPort.fromParts("127.0.0.1", 42000)).get) + val peer_1_bis = (peer_1._1, NodeAddress.from(HostAndPort.fromParts("127.0.0.1", 1112)).get) + val peer_2 = (randomKey.publicKey, Tor2("z4zif3fy7fe7bpg3", 4231)) + val peer_3 = (randomKey.publicKey, Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)) assert(db.listPeers().toSet === Set.empty) db.addOrUpdatePeer(peer_1._1, peer_1._2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index fd06e7589a..4ab0dbda28 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -20,6 +20,7 @@ import java.net.InetSocketAddress import akka.actor.ActorRef import akka.testkit.TestProbe +import com.google.common.net.HostAndPort import fr.acinq.eclair.randomBytes import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ @@ -62,7 +63,7 @@ class PeerSpec extends TestkitBaseClass { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, Set.empty)) - authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(NodeAddress(new InetSocketAddress("1.2.3.4", 42000))), None)) + authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(NodeAddress.from(HostAndPort.fromParts("1.2.3.4", 42000)).get), None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) From 67baabe899ba428830de3ae8d8283373f5e00d4f Mon Sep 17 00:00:00 2001 From: pm47 Date: Wed, 6 Feb 2019 16:28:39 +0100 Subject: [PATCH 34/40] fixed getinfo public addresses --- eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala | 4 ++-- eclair-core/src/test/resources/api/getinfo | 2 +- .../test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 5be673985c..a9ea3d376c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -279,7 +279,7 @@ class Setup(datadir: File, port = config.getInt("server.port"), chainHash = nodeParams.chainHash, blockHeight = Globals.blockCount.intValue(), - publicAddresses = nodeParams.publicAddresses.map(_.socketAddress.getHostString))) + publicAddresses = nodeParams.publicAddresses)) override def appKit: Kit = kit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala index 89038164a3..4a92ac039f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala @@ -41,7 +41,7 @@ import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router} -import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} +import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import fr.acinq.eclair.{Kit, ShortChannelId, feerateByte2Kw} import grizzled.slf4j.Logging import org.json4s.JsonAST.{JBool, JInt, JString} @@ -56,7 +56,7 @@ case class JsonRPCBody(jsonrpc: String = "1.0", id: String = "eclair-node", meth case class Error(code: Int, message: String) case class JsonRPCRes(result: AnyRef, error: Option[Error], id: String) case class Status(node_id: String) -case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int, publicAddresses: Seq[String]) +case class GetInfoResponse(nodeId: PublicKey, alias: String, port: Int, chainHash: BinaryData, blockHeight: Int, publicAddresses: Seq[NodeAddress]) case class AuditResponse(sent: Seq[PaymentSent], received: Seq[PaymentReceived], relayed: Seq[PaymentRelayed]) trait RPCRejection extends Rejection { def requestId: String diff --git a/eclair-core/src/test/resources/api/getinfo b/eclair-core/src/test/resources/api/getinfo index b30d616859..53cb58243f 100644 --- a/eclair-core/src/test/resources/api/getinfo +++ b/eclair-core/src/test/resources/api/getinfo @@ -1,6 +1,6 @@ { "result" : { - "publicAddresses" : [ "localhost" ], + "publicAddresses" : [ "localhost:9731" ], "alias" : "alice", "port" : 9735, "chainHash" : "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala index 9a95473af8..498de54887 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala @@ -212,7 +212,7 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { port = 9735, chainHash = Alice.nodeParams.chainHash, blockHeight = 123456, - publicAddresses = Alice.nodeParams.publicAddresses.map(_.socketAddress.getHostString) + publicAddresses = Alice.nodeParams.publicAddresses )) } import mockService.formats From c406f8c2e722a7576775edf47d27a49f6d1c8cd0 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Feb 2019 10:06:50 +0100 Subject: [PATCH 35/40] better client logs --- eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala | 3 ++- .../src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index e7c354ef75..d7a2bbfd00 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -71,7 +71,8 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Nod case Some(proxyParams) => val proxyAddress = peerOrProxyAddress val peerAddress = remoteAddress.socketAddress - log.info(s"connected to proxy ${str(proxyAddress)}") + log.info(s"connected to SOCKS5 proxy ${str(proxyAddress)}") + log.info(s"connecting to ${str(peerAddress)} via SOCKS5 ${str(proxyAddress)}") val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(peerAddress))) context become { case Tcp.CommandFailed(_: Socks5Connect) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index e3c74c42e0..32c35f7646 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -111,7 +111,6 @@ class Socks5Connection(connection: ActorRef, credentials_opt: Option[Credentials case _ => throw Socks5Error(s"Unrecognized address type") } context become connected - log.info(s"connected $connectedAddress") context.parent ! Socks5Connected(connectedAddress) isConnected = false } From d186033e24bc431a44596cee6e2aa9e33348c395 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Feb 2019 10:30:13 +0100 Subject: [PATCH 36/40] tor.stream-isolation->socks5.randomize-credentials and updated the doc --- TOR.md | 12 ++++++------ eclair-core/src/main/resources/reference.conf | 4 ++-- .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/TOR.md b/TOR.md index edb1558135..2b7fd4a6e7 100644 --- a/TOR.md +++ b/TOR.md @@ -108,12 +108,6 @@ value | description Tor protocol v3 (supported by Tor version 0.3.3.6 and higher) is backwards compatible and supports both v2 and v3 addresses. -To create a new Tor circuit for every connection, use `stream-isolation` parameter: - -``` -eclair.tor.stream-isolation = true -``` - For increased privacy do not advertise your IP address in the `server.public-ips` list, and set your binding IP to `localhost`: ``` eclair.server.binding-ip = "127.0.0.1" @@ -129,6 +123,12 @@ eclair.socks5.enabled = true You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6` or `eclair.socks5.use-for-tor` for fine tuning. +To create a new Tor circuit for every connection, use `stream-isolation` parameter: + +``` +eclair.socks5.randomize-credentials = true +``` + :warning: Tor hidden service and SOCKS5 are independent options. You can use just one of them, but if you want to get the most privacy features from using Tor use both. diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 8256f1c734..791f4cf45f 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -106,16 +106,16 @@ eclair { use-for-ipv4 = true use-for-ipv6 = true use-for-tor = true + randomize-credentials = false // this allows tor stream isolation } tor { enabled = false protocol = "v3" // v2, v3 - auth = "safecookie" // safecookie, password + auth = "password" // safecookie, password password = "foobar" // used when auth=password host = "127.0.0.1" port = 9051 private-key-file = "tor.dat" - stream-isolation = false } } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 45f75ae7d3..f7f8c28e99 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -188,7 +188,7 @@ object NodeParams { Some(Socks5ProxyParams( address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), credentials_opt = None, - randomizeCredentials = config.getBoolean("tor.stream-isolation"), + randomizeCredentials = config.getBoolean("socks5.randomize-credentials"), useForIPv4 = config.getBoolean("socks5.use-for-ipv4"), useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), useForTor = config.getBoolean("socks5.use-for-tor") From 0c9d3b4cc65cf673a82efb1d0fd1682b0abf4c3a Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Feb 2019 14:31:51 +0100 Subject: [PATCH 37/40] put back InetSocketAddress in Client Previous version made the Client aware of NodeAddress. --- .../scala/fr/acinq/eclair/NodeParams.scala | 3 +- .../fr/acinq/eclair/io/Authenticator.scala | 6 +-- .../scala/fr/acinq/eclair/io/Client.scala | 20 ++++---- .../main/scala/fr/acinq/eclair/io/Peer.scala | 49 ++++++++++--------- .../fr/acinq/eclair/io/Switchboard.scala | 9 ++-- .../acinq/eclair/tor/Socks5Connection.scala | 15 +++--- .../eclair/wire/LightningMessageTypes.scala | 38 ++++++-------- .../scala/fr/acinq/eclair/TestConstants.scala | 4 +- .../acinq/eclair/api/JsonRpcServiceSpec.scala | 2 +- .../eclair/api/JsonSerializersSpec.scala | 4 +- .../acinq/eclair/db/SqliteNetworkDbSpec.scala | 6 +-- .../acinq/eclair/db/SqlitePeersDbSpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 2 +- 13 files changed, 78 insertions(+), 84 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index f7f8c28e99..4849c0899c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -199,8 +199,7 @@ object NodeParams { val addresses = config.getStringList("server.public-ips") .toList - .map(ip => HostAndPort.fromParts(InetAddresses.forString(ip).getHostAddress, config.getInt("server.port"))) - .map(NodeAddress.from(_).get) ++ torAddress_opt + .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ torAddress_opt NodeParams( keyManager = keyManager, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala index c10974a5f8..0bb0fe3cb7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala @@ -90,12 +90,12 @@ object Authenticator { // @formatter:off sealed trait ConnectionDirection { def socketAddress: InetSocketAddress } - case class Incoming(address: InetSocketAddress) extends ConnectionDirection { override def socketAddress = address } - case class Outgoing(nodeAddress: NodeAddress) extends ConnectionDirection { override def socketAddress = nodeAddress.socketAddress } + case class Incoming(socketAddress: InetSocketAddress) extends ConnectionDirection + case class Outgoing(socketAddress: InetSocketAddress) extends ConnectionDirection case class OutgoingConnection(remoteNodeId: PublicKey, address: NodeAddress) case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: ConnectionDirection, origin_opt: Option[ActorRef]) - case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, address: ConnectionDirection, origin_opt: Option[ActorRef]) + case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, direction: ConnectionDirection, origin_opt: Option[ActorRef]) case class AuthenticationFailed(address: ConnectionDirection) extends RuntimeException(s"connection failed to $address") // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index d7a2bbfd00..1ac2e037d5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -35,7 +35,7 @@ import scala.concurrent.duration._ * Created by PM on 27/10/2015. * */ -class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { +class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]) extends Actor with DiagnosticActorLogging { import context.system @@ -49,9 +49,8 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Nod log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") proxyAddress case None => - val peerAddress = remoteAddress.socketAddress - log.info(s"connecting to ${str(peerAddress)}") - peerAddress + log.info(s"connecting to ${str(remoteAddress)}") + remoteAddress } IO(Tcp) ! Tcp.Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) context become connecting(addressToConnect) @@ -70,17 +69,16 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Nod nodeParams.socksProxy_opt match { case Some(proxyParams) => val proxyAddress = peerOrProxyAddress - val peerAddress = remoteAddress.socketAddress log.info(s"connected to SOCKS5 proxy ${str(proxyAddress)}") - log.info(s"connecting to ${str(peerAddress)} via SOCKS5 ${str(proxyAddress)}") - val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(peerAddress))) + log.info(s"connecting to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") + val proxy = context.actorOf(Socks5Connection.props(sender(), Socks5ProxyParams.proxyCredentials(proxyParams), Socks5Connect(remoteAddress))) context become { case Tcp.CommandFailed(_: Socks5Connect) => - log.info(s"connection failed to ${str(peerAddress)} via SOCKS5 ${str(proxyAddress)}") + log.info(s"connection failed to ${str(remoteAddress)} via SOCKS5 ${str(proxyAddress)}") origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self case Socks5Connected(_) => - log.info(s"connected to ${str(peerAddress)} via SOCKS5 proxy ${str(proxyAddress)}") + log.info(s"connected to ${str(remoteAddress)} via SOCKS5 proxy ${str(proxyAddress)}") auth(proxy) context become connected(proxy) } @@ -112,7 +110,7 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Nod object Client { - def props(nodeParams: NodeParams, authenticator: ActorRef, address: NodeAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) + def props(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocketAddress, remoteNodeId: PublicKey, origin_opt: Option[ActorRef]): Props = Props(new Client(nodeParams, authenticator, address, remoteNodeId, origin_opt)) - case class ConnectionFailed(address: NodeAddress) extends RuntimeException(s"connection failed to $address") + case class ConnectionFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 4afa65b0d0..1424d37b5d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -57,17 +57,16 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor when(DISCONNECTED) { case Event(Peer.Connect(NodeURI(_, hostAndPort)), d: DisconnectedData) => - NodeAddress.from(hostAndPort) match { - case Failure(_) => - sender ! "couldn't resolve address" - case Success(nodeAddress) if d.address_opt.contains(nodeAddress) => - // we already know this address, we'll reconnect automatically - sender ! "reconnection in progress" - case Success(nodeAddress) => - // we immediately process explicit connection requests to new addresses - context.actorOf(Client.props(nodeParams, authenticator, nodeAddress, remoteNodeId, origin_opt = Some(sender()))) + val address = new InetSocketAddress(hostAndPort.getHost, hostAndPort.getPort) + if (d.address_opt.contains(address)) { + // we already know this address, we'll reconnect automatically + sender ! "reconnection in progress" + stay + } else { + // we immediately process explicit connection requests to new addresses + context.actorOf(Client.props(nodeParams, authenticator, address, remoteNodeId, origin_opt = Some(sender()))) + stay } - stay case Event(Reconnect, d: DisconnectedData) => d.address_opt match { @@ -80,9 +79,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay using d.copy(attempts = d.attempts + 1) } - case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, address, origin_opt), d: DisconnectedData) => + case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, direction, origin_opt), d: DisconnectedData) => require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1") - log.debug(s"got authenticated connection to $remoteNodeId@${address.socketAddress.getHostString}:${address.socketAddress.getPort}") + log.debug(s"got authenticated connection to $remoteNodeId@${direction.socketAddress.getHostString}:${direction.socketAddress.getPort}") transport ! TransportHandler.Listener(self) context watch transport val localInit = nodeParams.overrideFeatures.get(remoteNodeId) match { @@ -92,11 +91,15 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor log.info(s"using globalFeatures=${localInit.globalFeatures} and localFeatures=${localInit.localFeatures}") transport ! localInit - val address_opt = address match { - case Authenticator.Outgoing(nodeAddress) => - // we store the ip upon successful outgoing connection, keeping only the most recent one - nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, nodeAddress) - Some(nodeAddress) + val address_opt = direction match { + case Authenticator.Outgoing(address) => + NodeAddress.fromParts(address.getHostString, address.getPort) match { + case Success(nodeAddress) => + // we store the ip upon successful outgoing connection, keeping only the most recent one + nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, nodeAddress) + case _ => () + } + Some(address) case Authenticator.Incoming(_) => None } goto(INITIALIZING) using InitializingData(address_opt, transport, d.channels, origin_opt, localInit) @@ -503,13 +506,13 @@ object Peer { case class FinalChannelId(id: BinaryData) extends ChannelId sealed trait Data { - def address_opt: Option[NodeAddress] + def address_opt: Option[InetSocketAddress] def channels: Map[_ <: ChannelId, ActorRef] // will be overridden by Map[FinalChannelId, ActorRef] or Map[ChannelId, ActorRef] } case class Nothing() extends Data { override def address_opt = None; override def channels = Map.empty } - case class DisconnectedData(address_opt: Option[NodeAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0) extends Data - case class InitializingData(address_opt: Option[NodeAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef], localInit: wire.Init) extends Data - case class ConnectedData(address_opt: Option[NodeAddress], transport: ActorRef, localInit: wire.Init, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data + case class DisconnectedData(address_opt: Option[InetSocketAddress], channels: Map[FinalChannelId, ActorRef], attempts: Int = 0) extends Data + case class InitializingData(address_opt: Option[InetSocketAddress], transport: ActorRef, channels: Map[FinalChannelId, ActorRef], origin_opt: Option[ActorRef], localInit: wire.Init) extends Data + case class ConnectedData(address_opt: Option[InetSocketAddress], transport: ActorRef, localInit: wire.Init, remoteInit: wire.Init, channels: Map[ChannelId, ActorRef], gossipTimestampFilter: Option[GossipTimestampFilter] = None, behavior: Behavior = Behavior(), expectedPong_opt: Option[ExpectedPong] = None) extends Data case class ExpectedPong(ping: Ping, timestamp: Long = Platform.currentTime) case class PingTimeout(ping: Ping) @@ -519,7 +522,7 @@ object Peer { case object INITIALIZING extends State case object CONNECTED extends State - case class Init(previousKnownAddress: Option[NodeAddress], storedChannels: Set[HasCommitments]) + case class Init(previousKnownAddress: Option[InetSocketAddress], storedChannels: Set[HasCommitments]) case class Connect(uri: NodeURI) case object Reconnect case object Disconnect @@ -533,7 +536,7 @@ object Peer { } case object GetPeerInfo case object SendPing - case class PeerInfo(nodeId: PublicKey, state: String, address: Option[NodeAddress], channels: Int) + case class PeerInfo(nodeId: PublicKey, state: String, address: Option[InetSocketAddress], channels: Int) case class PeerRoutingMessage(transport: ActorRef, remoteNodeId: PublicKey, message: RoutingMessage) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index e0347db14e..140b7db082 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -16,6 +16,8 @@ package fr.acinq.eclair.io +import java.net.InetSocketAddress + import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Status, SupervisorStrategy, Terminated} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.NodeParams @@ -25,7 +27,7 @@ import fr.acinq.eclair.payment.Relayer.RelayPayload import fr.acinq.eclair.payment.{Relayed, Relayer} import fr.acinq.eclair.router.Rebroadcast import fr.acinq.eclair.transactions.{IN, OUT} -import fr.acinq.eclair.wire.{NodeAddress, TemporaryNodeFailure, UpdateAddHtlc} +import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc} import grizzled.slf4j.Logging import scala.util.Success @@ -58,8 +60,9 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto case (remoteNodeId, states) => (remoteNodeId, states, peers.get(remoteNodeId)) } .map { - case (remoteNodeId, states, address_opt) => + case (remoteNodeId, states, nodeaddress_opt) => // we might not have an address if we didn't initiate the connection in the first place + val address_opt = nodeaddress_opt.map(_.socketAddress) val peer = createOrGetPeer(Map(), remoteNodeId, previousKnownAddress = address_opt, offlineChannels = states.toSet) remoteNodeId -> peer }.toMap @@ -112,7 +115,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto * @param offlineChannels * @return */ - def createOrGetPeer(peers: Map[PublicKey, ActorRef], remoteNodeId: PublicKey, previousKnownAddress: Option[NodeAddress], offlineChannels: Set[HasCommitments]) = { + def createOrGetPeer(peers: Map[PublicKey, ActorRef], remoteNodeId: PublicKey, previousKnownAddress: Option[InetSocketAddress], offlineChannels: Set[HasCommitments]) = { peers.get(remoteNodeId) match { case Some(peer) => peer case None => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index 32c35f7646..c62f19c740 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -26,6 +26,8 @@ import fr.acinq.eclair.randomBytes import fr.acinq.eclair.tor.Socks5Connection.{Credentials, Socks5Connect} import fr.acinq.eclair.wire._ +import scala.util.Success + /** * Simple socks 5 client. It should be given a new connection, and will * @@ -221,13 +223,12 @@ case class Socks5ProxyParams(address: InetSocketAddress, credentials_opt: Option object Socks5ProxyParams { - def proxyAddress(remoteAddress: NodeAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = - remoteAddress match { - case _: IPv4 if proxyParams.useForIPv4 => Some(proxyParams.address) - case _: IPv6 if proxyParams.useForIPv6 => Some(proxyParams.address) - case _: Tor2 if proxyParams.useForTor => Some(proxyParams.address) - case _: Tor3 if proxyParams.useForTor => Some(proxyParams.address) - case _ => None + def proxyAddress(socketAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = + NodeAddress.fromParts(socketAddress.getHostString, socketAddress.getPort).toOption map { + case _: IPv4 if proxyParams.useForIPv4 => proxyParams.address + case _: IPv6 if proxyParams.useForIPv6 => proxyParams.address + case _: Tor2 if proxyParams.useForTor => proxyParams.address + case _: Tor3 if proxyParams.useForTor => proxyParams.address } def proxyCredentials(proxyParams: Socks5ProxyParams): Option[Socks5Connection.Credentials] = diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index c738f127e2..161ea210d2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -167,43 +167,33 @@ sealed trait NodeAddress { def socketAddress: InetSocketAddress } sealed trait OnionAddress extends NodeAddress object NodeAddress { /** - * Creates a NodeAddress from a HostPort. + * Creates a NodeAddress from a host and port. * * Note that non-onion hosts will be resolved. * * We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on * the .onion TLD and rely on their length to separate v2/v3. * - * @param hostPort + * @param host + * @param port * @return */ - def from(hostPort: HostAndPort): Try[NodeAddress] = Try { - hostPort match { - case _ if hostPort.getHost.endsWith(".onion") && hostPort.getHost.length == 22 => Tor2(hostPort.getHost.dropRight(6), hostPort.getPort) - case _ if hostPort.getHost.endsWith(".onion") && hostPort.getHost.length == 62 => Tor3(hostPort.getHost.dropRight(6), hostPort.getPort) - case _ => InetAddress.getByName(hostPort.getHost) match { - case a: Inet4Address => IPv4(a, hostPort.getPort) - case a: Inet6Address => IPv6(a, hostPort.getPort) + def fromParts(host: String, port: Int): Try[NodeAddress] = Try { + host match { + case _ if host.endsWith(".onion") && host.length == 22 => Tor2(host.dropRight(6), port) + case _ if host.endsWith(".onion") && host.length == 62 => Tor3(host.dropRight(6), port) + case _ => InetAddress.getByName(host) match { + case a: Inet4Address => IPv4(a, port) + case a: Inet6Address => IPv6(a, port) } } } } +case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv4, port) } +case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv6, port) } +case class Tor2(tor2: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) } +case class Tor3(tor3: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) } // @formatter:on -case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { - override def socketAddress = new InetSocketAddress(ipv4, port) -} - -case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { - override def socketAddress = new InetSocketAddress(ipv6, port) -} - -case class Tor2(tor2: String, port: Int) extends OnionAddress { - override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) -} - -case class Tor3(tor3: String, port: Int) extends OnionAddress { - override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) -} case class NodeAnnouncement(signature: BinaryData, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 0a2878624f..c585efeb9f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -48,7 +48,7 @@ object TestConstants { keyManager = keyManager, alias = "alice", color = Color(1, 2, 3), - publicAddresses = NodeAddress.from(HostAndPort.fromParts("localhost", 9731)).get :: Nil, + publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, globalFeatures = "", localFeatures = "00", overrideFeatures = Map.empty, @@ -109,7 +109,7 @@ object TestConstants { keyManager = keyManager, alias = "bob", color = Color(4, 5, 6), - publicAddresses = NodeAddress.from(HostAndPort.fromParts("localhost", 9732)).get :: Nil, + publicAddresses = NodeAddress.fromParts("localhost", 9732).get :: Nil, globalFeatures = "", localFeatures = "00", // no announcement overrideFeatures = Map.empty, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala index 498de54887..c90de30251 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonRpcServiceSpec.scala @@ -159,7 +159,7 @@ class JsonRpcServiceSpec extends FunSuite with ScalatestRouteTest { case GetPeerInfo => sender() ! PeerInfo( nodeId = Alice.nodeParams.nodeId, state = "CONNECTED", - address = Some(Alice.nodeParams.publicAddresses.head), + address = Some(Alice.nodeParams.publicAddresses.head.socketAddress), channels = 1) } })) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala index 99c0564b63..deba881b91 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/api/JsonSerializersSpec.scala @@ -51,8 +51,8 @@ class JsonSerializersSpec extends FunSuite with Matchers { } test("NodeAddress serialization") { - val ipv4 = NodeAddress.from(HostAndPort.fromParts("10.0.0.1", 8888)).get - val ipv6LocalHost = NodeAddress.from(HostAndPort.fromParts(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)).getHostAddress, 9735)).get + val ipv4 = NodeAddress.fromParts("10.0.0.1", 8888).get + val ipv6LocalHost = NodeAddress.fromParts(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)).getHostAddress, 9735).get val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 98d4dc4292..341b584d8f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -43,9 +43,9 @@ class SqliteNetworkDbSpec extends FunSuite { val sqlite = inmem val db = new SqliteNetworkDb(sqlite) - val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) - val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) - val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.from(HostAndPort.fromParts("192.168.1.42", 42000)).get :: Nil) + val node_1 = Announcements.makeNodeAnnouncement(randomKey, "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) + val node_2 = Announcements.makeNodeAnnouncement(randomKey, "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) + val node_3 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil) val node_4 = Announcements.makeNodeAnnouncement(randomKey, "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil) assert(db.listNodes().toSet === Set.empty) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala index 64db06e7c6..f6b9a78534 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqlitePeersDbSpec.scala @@ -40,8 +40,8 @@ class SqlitePeersDbSpec extends FunSuite { val sqlite = inmem val db = new SqlitePeersDb(sqlite) - val peer_1 = (randomKey.publicKey, NodeAddress.from(HostAndPort.fromParts("127.0.0.1", 42000)).get) - val peer_1_bis = (peer_1._1, NodeAddress.from(HostAndPort.fromParts("127.0.0.1", 1112)).get) + val peer_1 = (randomKey.publicKey, NodeAddress.fromParts("127.0.0.1", 42000).get) + val peer_1_bis = (peer_1._1, NodeAddress.fromParts("127.0.0.1", 1112).get) val peer_2 = (randomKey.publicKey, Tor2("z4zif3fy7fe7bpg3", 4231)) val peer_3 = (randomKey.publicKey, Tor3("mrl2d3ilhctt2vw4qzvmz3etzjvpnc6dczliq5chrxetthgbuczuggyd", 4231)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 4ab0dbda28..fce666800b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -63,7 +63,7 @@ class PeerSpec extends TestkitBaseClass { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, Set.empty)) - authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(NodeAddress.from(HostAndPort.fromParts("1.2.3.4", 42000)).get), None)) + authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(new InetSocketAddress("1.2.3.4", 42000)), None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) From 69b813bf5a1926851248c5b5f3dfcd86acf382e1 Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Feb 2019 15:15:15 +0100 Subject: [PATCH 38/40] added test to socks5 proxy --- .../scala/fr/acinq/eclair/io/Client.scala | 27 +++++---- .../acinq/eclair/tor/Socks5Connection.scala | 2 +- .../eclair/tor/Socks5ConnectionSpec.scala | 59 +++++++++++++++++++ 3 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 1ac2e037d5..13efa5d46c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -44,29 +44,29 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine def receive: Receive = { case 'connect => - val addressToConnect = nodeParams.socksProxy_opt.flatMap(proxyParams => Socks5ProxyParams.proxyAddress(remoteAddress, proxyParams)) match { - case Some(proxyAddress) => + val (peerOrProxyAddress, proxyParams_opt) = nodeParams.socksProxy_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteAddress, proxyParams))) match { + case Some((proxyParams, Some(proxyAddress))) => log.info(s"connecting to SOCKS5 proxy ${str(proxyAddress)}") - proxyAddress - case None => + (proxyAddress, Some(proxyParams)) + case _ => log.info(s"connecting to ${str(remoteAddress)}") - remoteAddress + (remoteAddress, None) } - IO(Tcp) ! Tcp.Connect(addressToConnect, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) - context become connecting(addressToConnect) + IO(Tcp) ! Tcp.Connect(peerOrProxyAddress, timeout = Some(50 seconds), options = KeepAlive(true) :: Nil, pullMode = true) + context become connecting(proxyParams_opt) } - def connecting(to: InetSocketAddress): Receive = { - case Tcp.CommandFailed(_: Tcp.Connect) => - log.info(s"connection failed to ${str(to)}") + def connecting(proxyParams: Option[Socks5ProxyParams]): Receive = { + case Tcp.CommandFailed(c: Tcp.Connect) => + val peerOrProxyAddress = c.remoteAddress + log.info(s"connection failed to ${str(peerOrProxyAddress)}") origin_opt.map(_ ! Status.Failure(ConnectionFailed(remoteAddress))) context stop self case Tcp.Connected(peerOrProxyAddress, _) => val connection = sender() context watch connection - - nodeParams.socksProxy_opt match { + proxyParams match { case Some(proxyParams) => val proxyAddress = peerOrProxyAddress log.info(s"connected to SOCKS5 proxy ${str(proxyAddress)}") @@ -83,7 +83,8 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine context become connected(proxy) } case None => - log.info(s"connected to ${str(to)}") + val peerAddress = peerOrProxyAddress + log.info(s"connected to ${str(peerAddress)}") auth(connection) context become connected(connection) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala index c62f19c740..48587a349a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala @@ -224,7 +224,7 @@ case class Socks5ProxyParams(address: InetSocketAddress, credentials_opt: Option object Socks5ProxyParams { def proxyAddress(socketAddress: InetSocketAddress, proxyParams: Socks5ProxyParams): Option[InetSocketAddress] = - NodeAddress.fromParts(socketAddress.getHostString, socketAddress.getPort).toOption map { + NodeAddress.fromParts(socketAddress.getHostString, socketAddress.getPort).toOption collect { case _: IPv4 if proxyParams.useForIPv4 => proxyParams.address case _: IPv6 if proxyParams.useForIPv6 => proxyParams.address case _: Tor2 if proxyParams.useForTor => proxyParams.address diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala new file mode 100644 index 0000000000..0ea4b0230e --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2018 ACINQ SAS + * + * 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. + */ + +package fr.acinq.eclair.tor + +import java.net.InetSocketAddress + +import org.scalatest.FunSuite + +/** + * Created by PM on 27/01/2017. + */ + +class Socks5ConnectionSpec extends FunSuite { + + test("get proxy address") { + val proxyAddress = new InetSocketAddress(9050) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("1.2.3.4", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true)) == Some(proxyAddress)) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("1.2.3.4", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true)) == None) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true)) == Some(proxyAddress)) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = false, useForTor = true)) == None) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true)) == Some(proxyAddress)) + + assert(Socks5ProxyParams.proxyAddress( + socketAddress = new InetSocketAddress("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735), + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = false)) == None) + + + } + +} From 3567622161d55edb01c2a1e668c1e0bd40f51ace Mon Sep 17 00:00:00 2001 From: pm47 Date: Thu, 7 Feb 2019 17:05:12 +0100 Subject: [PATCH 39/40] reverted changes made to Authenticator --- .../fr/acinq/eclair/io/Authenticator.scala | 21 ++++++--------- .../scala/fr/acinq/eclair/io/Client.scala | 2 +- .../main/scala/fr/acinq/eclair/io/Peer.scala | 26 ++++++++----------- .../scala/fr/acinq/eclair/io/Server.scala | 2 +- .../fr/acinq/eclair/io/Switchboard.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 3 +-- 6 files changed, 23 insertions(+), 33 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala index 0bb0fe3cb7..750d397be5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Authenticator.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Noise.KeyPair import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.crypto.TransportHandler.HandshakeCompleted import fr.acinq.eclair.io.Authenticator.{Authenticated, AuthenticationFailed, PendingAuth} -import fr.acinq.eclair.wire.{LightningMessageCodecs, NodeAddress} +import fr.acinq.eclair.wire.LightningMessageCodecs import fr.acinq.eclair.{Logs, NodeParams} /** @@ -42,7 +42,7 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo def ready(switchboard: ActorRef, authenticating: Map[ActorRef, PendingAuth]): Receive = { case pending@PendingAuth(connection, remoteNodeId_opt, address, _) => - log.debug(s"authenticating connection to ${address.socketAddress.getHostString}:${address.socketAddress.getPort} (pending=${authenticating.size} handlers=${context.children.size})") + log.debug(s"authenticating connection to ${address.getHostString}:${address.getPort} (pending=${authenticating.size} handlers=${context.children.size})") val transport = context.actorOf(TransportHandler.props( KeyPair(nodeParams.nodeId.toBin, nodeParams.privateKey.toBin), remoteNodeId_opt.map(_.toBin), @@ -55,8 +55,8 @@ class Authenticator(nodeParams: NodeParams) extends Actor with DiagnosticActorLo val pendingAuth = authenticating(transport) import pendingAuth.{address, remoteNodeId_opt} val outgoing = remoteNodeId_opt.isDefined - log.info(s"connection authenticated with $remoteNodeId@${address.socketAddress.getHostString}:${address.socketAddress.getPort} direction=${if (outgoing) "outgoing" else "incoming"}") - switchboard ! Authenticated(connection, transport, remoteNodeId, address, pendingAuth.origin_opt) + log.info(s"connection authenticated with $remoteNodeId@${address.getHostString}:${address.getPort} direction=${if (outgoing) "outgoing" else "incoming"}") + switchboard ! Authenticated(connection, transport, remoteNodeId, address, remoteNodeId_opt.isDefined, pendingAuth.origin_opt) context become ready(switchboard, authenticating - transport) case Terminated(transport) => @@ -88,15 +88,10 @@ object Authenticator { def props(nodeParams: NodeParams): Props = Props(new Authenticator(nodeParams)) // @formatter:off - - sealed trait ConnectionDirection { def socketAddress: InetSocketAddress } - case class Incoming(socketAddress: InetSocketAddress) extends ConnectionDirection - case class Outgoing(socketAddress: InetSocketAddress) extends ConnectionDirection - - case class OutgoingConnection(remoteNodeId: PublicKey, address: NodeAddress) - case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: ConnectionDirection, origin_opt: Option[ActorRef]) - case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, direction: ConnectionDirection, origin_opt: Option[ActorRef]) - case class AuthenticationFailed(address: ConnectionDirection) extends RuntimeException(s"connection failed to $address") + case class OutgoingConnection(remoteNodeId: PublicKey, address: InetSocketAddress) + case class PendingAuth(connection: ActorRef, remoteNodeId_opt: Option[PublicKey], address: InetSocketAddress, origin_opt: Option[ActorRef]) + case class Authenticated(connection: ActorRef, transport: ActorRef, remoteNodeId: PublicKey, address: InetSocketAddress, outgoing: Boolean, origin_opt: Option[ActorRef]) + case class AuthenticationFailed(address: InetSocketAddress) extends RuntimeException(s"connection failed to $address") // @formatter:on } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala index 13efa5d46c..837cfe0b50 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala @@ -106,7 +106,7 @@ class Client(nodeParams: NodeParams, authenticator: ActorRef, remoteAddress: Ine private def str(address: InetSocketAddress): String = s"${address.getHostString}:${address.getPort}" - def auth(connection: ActorRef) = authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = Authenticator.Outgoing(remoteAddress), origin_opt = origin_opt) + def auth(connection: ActorRef) = authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = Some(remoteNodeId), address = remoteAddress, origin_opt = origin_opt) } object Client { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 1424d37b5d..9b716cfb09 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -34,7 +34,7 @@ import scodec.Attempt import scala.compat.Platform import scala.concurrent.duration._ -import scala.util.{Failure, Random, Success} +import scala.util.Random /** * Created by PM on 26/08/2016. @@ -79,9 +79,9 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay using d.copy(attempts = d.attempts + 1) } - case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, direction, origin_opt), d: DisconnectedData) => + case Event(Authenticator.Authenticated(_, transport, remoteNodeId1, address, outgoing, origin_opt), d: DisconnectedData) => require(remoteNodeId == remoteNodeId1, s"invalid nodeid: $remoteNodeId != $remoteNodeId1") - log.debug(s"got authenticated connection to $remoteNodeId@${direction.socketAddress.getHostString}:${direction.socketAddress.getPort}") + log.debug(s"got authenticated connection to $remoteNodeId@${address.getHostString}:${address.getPort}") transport ! TransportHandler.Listener(self) context watch transport val localInit = nodeParams.overrideFeatures.get(remoteNodeId) match { @@ -91,17 +91,13 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor log.info(s"using globalFeatures=${localInit.globalFeatures} and localFeatures=${localInit.localFeatures}") transport ! localInit - val address_opt = direction match { - case Authenticator.Outgoing(address) => - NodeAddress.fromParts(address.getHostString, address.getPort) match { - case Success(nodeAddress) => - // we store the ip upon successful outgoing connection, keeping only the most recent one - nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, nodeAddress) - case _ => () - } - Some(address) - case Authenticator.Incoming(_) => None - } + val address_opt = if (outgoing) { + // we store the node address upon successful outgoing connection, so we can reconnect later + // any previous address is overwritten + NodeAddress.fromParts(address.getHostString, address.getPort).map(nodeAddress => nodeParams.peersDb.addOrUpdatePeer(remoteNodeId, nodeAddress)) + Some(address) + } else None + goto(INITIALIZING) using InitializingData(address_opt, transport, d.channels, origin_opt, localInit) case Event(Terminated(actor), d@DisconnectedData(_, channels, _)) if channels.exists(_._2 == actor) => @@ -146,7 +142,7 @@ class Peer(nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: Actor stay } - case Event(Authenticator.Authenticated(connection, _, _, _, origin_opt), _) => + case Event(Authenticator.Authenticated(connection, _, _, _, _, origin_opt), _) => // two connections in parallel origin_opt.foreach(origin => origin ! Status.Failure(new RuntimeException("there is another connection attempt in progress"))) // we kill this one diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala index 1bec4433b9..3e399b3709 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Server.scala @@ -57,7 +57,7 @@ class Server(nodeParams: NodeParams, authenticator: ActorRef, address: InetSocke case Connected(remote, _) => log.info(s"connected to $remote") val connection = sender - authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = None, address = Authenticator.Incoming(remote), origin_opt = None) + authenticator ! Authenticator.PendingAuth(connection, remoteNodeId_opt = None, address = remote, origin_opt = None) listener ! ResumeAccepting(batchSize = 1) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 140b7db082..8d8ec49201 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -95,7 +95,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto context become main(peers - remoteNodeId) } - case auth@Authenticator.Authenticated(_, _, remoteNodeId, _, _) => + case auth@Authenticator.Authenticated(_, _, remoteNodeId, _, _, _) => // if this is an incoming connection, we might not yet have created the peer val peer = createOrGetPeer(peers, remoteNodeId, previousKnownAddress = None, offlineChannels = Set.empty) peer forward auth diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index fce666800b..d318e75bd0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -26,7 +26,6 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair.blockchain.EclairWallet import fr.acinq.eclair.crypto.TransportHandler -import fr.acinq.eclair.io.Authenticator.Outgoing import fr.acinq.eclair.io.Peer.{CHANNELID_ZERO, ResumeAnnouncements, SendPing} import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo import fr.acinq.eclair.router.{ChannelRangeQueries, ChannelRangeQueriesSpec, Rebroadcast} @@ -63,7 +62,7 @@ class PeerSpec extends TestkitBaseClass { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, Set.empty)) - authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, Outgoing(new InetSocketAddress("1.2.3.4", 42000)), None)) + authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, new InetSocketAddress("1.2.3.4", 42000), outgoing = true, None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) From 517d9ec6f6ec49beb23497224adff401951588d1 Mon Sep 17 00:00:00 2001 From: rorp Date: Thu, 7 Feb 2019 11:14:03 -0800 Subject: [PATCH 40/40] typo --- TOR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TOR.md b/TOR.md index 2b7fd4a6e7..7f84369393 100644 --- a/TOR.md +++ b/TOR.md @@ -123,7 +123,7 @@ eclair.socks5.enabled = true You can use SOCKS5 proxy only for specific types of addresses. Use `eclair.socks5.use-for-ipv4`, `eclair.socks5.use-for-ipv6` or `eclair.socks5.use-for-tor` for fine tuning. -To create a new Tor circuit for every connection, use `stream-isolation` parameter: +To create a new Tor circuit for every connection, use `randomize-credentials` parameter: ``` eclair.socks5.randomize-credentials = true