From 2e61e336de24d81d7df09ab3d71b2fbf49852586 Mon Sep 17 00:00:00 2001 From: rorp Date: Tue, 31 May 2022 19:22:30 -0700 Subject: [PATCH] Drop support for Tor v2 hidden services --- docs/Tor.md | 16 +--- docs/release-notes/eclair-vnext.md | 7 +- eclair-core/src/main/resources/reference.conf | 1 - .../main/scala/fr/acinq/eclair/Setup.scala | 2 - .../acinq/eclair/tor/TorProtocolHandler.scala | 75 ++++++------------- .../eclair/tor/TorProtocolHandlerSpec.scala | 49 ++++-------- 6 files changed, 45 insertions(+), 105 deletions(-) diff --git a/docs/Tor.md b/docs/Tor.md index 95ffea146d..377e43a610 100644 --- a/docs/Tor.md +++ b/docs/Tor.md @@ -1,5 +1,7 @@ ## How to Use Tor with Eclair +Current supported version of Tor is 0.3.3.6 or higher. + ### Installing Tor on your node #### Linux: @@ -100,21 +102,7 @@ eclair-cli getinfo ``` Eclair saves the Tor endpoint's private key in `~/.eclair/tor.dat`, so that it can recreate the endpoint address after a 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. - 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" diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 97bd4cc0e1..31392c7d38 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -4,7 +4,12 @@ ## Major changes - +Dropped support for version 2 of Tor protocol. That means + +- Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore +- Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon + +IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before upgrading to this release. ### API changes diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 56b1961eba..ae606c4774 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -315,7 +315,6 @@ eclair { tor { enabled = false - protocol = "v3" // v2, v3 auth = "password" // safecookie, password password = "foobar" // used when auth=password host = "127.0.0.1" 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 446032024f..ecbe266f0e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -43,7 +43,6 @@ import fr.acinq.eclair.payment.receive.PaymentHandler import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.payment.send.{Autoprobe, PaymentInitiator} import fr.acinq.eclair.router._ -import fr.acinq.eclair.tor.TorProtocolHandler.OnionServiceVersion import fr.acinq.eclair.tor.{Controller, TorProtocolHandler} import fr.acinq.eclair.wire.protocol.NodeAddress import grizzled.slf4j.Logging @@ -351,7 +350,6 @@ class Setup(val datadir: File, case "safecookie" => TorProtocolHandler.SafeCookie() } val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion(config.getString("tor.protocol")), authentication = auth, privateKeyPath = new File(datadir, config.getString("tor.private-key-file")).toPath, virtualPort = config.getInt("server.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 c6f1584ded..b775280649 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 @@ -16,20 +16,19 @@ package fr.acinq.eclair.tor -import java.nio.file.attribute.PosixFilePermissions -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.eclair.tor.TorProtocolHandler.{Authentication, OnionServiceVersion} -import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor2, Tor3} -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec +import fr.acinq.eclair.tor.TorProtocolHandler.Authentication +import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor3} import scodec.bits.Bases.Alphabets import scodec.bits.ByteVector +import java.nio.file.attribute.PosixFilePermissions +import java.nio.file.{Files, Path, Paths} +import java.util +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import scala.concurrent.Promise import scala.util.Try @@ -40,15 +39,13 @@ case class TorException(private val msg: String) extends RuntimeException(s"Tor * * Specification: https://gitweb.torproject.org/torspec.git/tree/control-spec.txt * - * @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 for the public hidden service (typically 9735) * @param targets address of our protected server (format [host:]port), 127.0.0.1:[[virtualPort]] if empty * @param onionAdded a Promise to track creation of the endpoint */ -class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, - authentication: Authentication, +class TorProtocolHandler(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String], @@ -74,8 +71,8 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, 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 (!isCompatible(torVersion)) { + throw TorException(s"unsupported Tor version: $torVersion") } if (!Authentication.isCompatible(authentication, methods)) { throw TorException(s"cannot use authentication '$authentication', supported methods are '$methods'") @@ -116,10 +113,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, val res = readResponse(data) if (ok(res)) { val serviceId = processOnionResponse(parseResponse(res)) - address = Some(onionServiceVersion match { - case V2 => Tor2(serviceId, virtualPort) - case V3 => Tor3(serviceId, virtualPort) - }) + address = Some(Tor3(serviceId, virtualPort)) onionAdded.foreach(_.success(address.get)) log.debug("Onion address: {}", address.get) } @@ -151,10 +145,7 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, if (privateKeyPath.toFile.exists()) { readString(privateKeyPath) } else { - onionServiceVersion match { - case V2 => "NEW:RSA1024" - case V3 => "NEW:ED25519-V3" - } + "NEW:ED25519-V3" } } @@ -190,48 +181,30 @@ class TorProtocolHandler(onionServiceVersion: OnionServiceVersion, } object TorProtocolHandler { - def props(version: OnionServiceVersion, - authentication: Authentication, + def props(authentication: Authentication, privateKeyPath: Path, virtualPort: Int, targets: Seq[String] = Seq(), onionAdded: Option[Promise[NodeAddress]] = None ): Props = - Props(new TorProtocolHandler(version, authentication, privateKeyPath, virtualPort, targets, onionAdded)) + Props(new TorProtocolHandler(authentication, privateKeyPath, virtualPort, targets, onionAdded)) // those are defined in the spec private val ServerKey = ByteVector.view("Tor safe cookie authentication server-to-controller hash".getBytes()) private val ClientKey = ByteVector.view("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 + private[tor] def isCompatible(torVersion: String): Boolean = + 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 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 9b342051e5..2f520890c9 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 @@ -54,7 +54,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandlerProps = TorProtocolHandler.props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -70,7 +69,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -86,31 +84,15 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString(s"""AUTHENTICATE "$PASSWORD"\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(Tor2("z4zif3fy7fe7bpg3", 9999))) + awaitCond(promiseOnionAddress.isCompleted) - val address = Await.result(promiseOnionAddress.future, 3 seconds) - assert(address === Tor2("z4zif3fy7fe7bpg3", 9999)) - - assert(readString(PkFilePath) === "RSA1024:private-key") + assertThrows[TorException](Await.result(promiseOnionAddress.future, Duration.Inf)) } test("happy path v3") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v3"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -148,20 +130,18 @@ class TorProtocolHandlerSpec extends TestKitBaseClass } 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")) - + assert(isCompatible("0.3.3.6")) + assert(!isCompatible("0.3.3.5")) + assert(isCompatible("0.3.3.6-devel")) + assert(isCompatible("0.4")) + assert(!isCompatible("0.2")) + assert(isCompatible("0.5.1.2.3.4")) } test("authentication method errors") { val promiseOnionAddress = Promise[NodeAddress]() val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = Password(PASSWORD), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -173,7 +153,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass 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-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -188,7 +168,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, fr.acinq.eclair.randomBytes32().toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -200,7 +179,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass 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-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -221,7 +200,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -233,7 +211,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass 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-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -258,7 +236,6 @@ class TorProtocolHandlerSpec extends TestKitBaseClass Files.write(CookieFilePath, AuthCookie.toArray) val protocolHandler = TestActorRef(props( - version = OnionServiceVersion("v2"), authentication = SafeCookie(ClientNonce), privateKeyPath = PkFilePath, virtualPort = 9999, @@ -270,7 +247,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass 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-VERSION Tor=\"0.3.3.6\"\r\n" + "250 OK\r\n" ) @@ -284,7 +261,7 @@ class TorProtocolHandlerSpec extends TestKitBaseClass "250 OK\r\n" ) - expectMsg(ByteString("ADD_ONION NEW:RSA1024 Port=9999,9999\r\n")) + expectMsg(ByteString("ADD_ONION NEW:ED25519-V3 Port=9999,9999\r\n")) protocolHandler ! ByteString( "513 Invalid argument\r\n" )