Skip to content

Commit

Permalink
Drop support for Tor v2 hidden services (#2296)
Browse files Browse the repository at this point in the history
Tor v2 addresses have been officially deprecated by the
Tor team and removed from the lightning specification in
lightning/bolts#940
  • Loading branch information
rorp authored Jun 6, 2022
1 parent 9610fe3 commit 47c5b95
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 105 deletions.
16 changes: 2 additions & 14 deletions docs/Tor.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 6 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

## Major changes

<insert 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

Expand Down
1 change: 0 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 0 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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],
Expand All @@ -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'")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"
}
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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"
)

Expand All @@ -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,
Expand All @@ -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"
)

Expand All @@ -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,
Expand All @@ -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"
)

Expand All @@ -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,
Expand All @@ -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"
)

Expand All @@ -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"
)
Expand Down

0 comments on commit 47c5b95

Please sign in to comment.