mirror of
https://github.com/ACINQ/eclair.git
synced 2024-11-19 01:43:22 +01:00
Drop support for Tor v2 hidden services (#2296)
Tor v2 addresses have been officially deprecated by the Tor team and removed from the lightning specification in https://github.com/lightning/bolts/pull/940
This commit is contained in:
parent
9610fe30e3
commit
47c5b95eaa
16
docs/Tor.md
16
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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
||||
awaitCond(promiseOnionAddress.isCompleted)
|
||||
|
||||
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)))
|
||||
|
||||
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"
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user