From bb6148e31cdce5162d44922d9dfb136e02c4c0d8 Mon Sep 17 00:00:00 2001 From: Richard Myers Date: Tue, 16 Aug 2022 09:01:47 +0200 Subject: [PATCH] Support DNS hostnames in node announcements (#2234) It is now possible to specify a DNS host name as one of your `server.public-ips` addresses. DNS host names will not be resolved until eclair attempts to connect to the peer. See https://github.com/lightning/bolts/pull/911 --- docs/release-notes/eclair-vnext.md | 4 ++ eclair-core/src/main/resources/reference.conf | 1 + .../scala/fr/acinq/eclair/NodeParams.scala | 19 +++++++- .../scala/fr/acinq/eclair/io/Client.scala | 3 +- .../fr/acinq/eclair/io/ReconnectionTask.scala | 2 +- .../acinq/eclair/router/Announcements.scala | 4 ++ .../fr/acinq/eclair/router/Validation.scala | 10 ++++- .../acinq/eclair/tor/Socks5Connection.scala | 3 +- .../eclair/wire/protocol/CommonCodecs.scala | 1 + .../wire/protocol/LightningMessageTypes.scala | 24 ++++++++-- .../scala/fr/acinq/eclair/StartupSpec.scala | 24 ++++++++++ .../watchdogs/BlockchainWatchdogSpec.scala | 3 +- .../fr/acinq/eclair/db/NetworkDbSpec.scala | 11 +++-- .../fr/acinq/eclair/io/NodeURISpec.scala | 4 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 16 +++++++ .../eclair/json/JsonSerializersSpec.scala | 2 + .../eclair/router/AnnouncementsSpec.scala | 34 ++++++++++++-- .../fr/acinq/eclair/router/RouterSpec.scala | 45 +++++++++++++++++++ .../eclair/tor/Socks5ConnectionSpec.scala | 22 ++++++--- .../wire/protocol/CommonCodecsSpec.scala | 7 +++ .../fr/acinq/eclair/api/ApiServiceSpec.scala | 4 +- 21 files changed, 215 insertions(+), 28 deletions(-) diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 36e9456b3e..6725d0135a 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -143,6 +143,10 @@ eclair.on-chain-fees.spend-anchor-without-htlcs = false This is disabled by default, because there is still a risk of losing funds until bitcoin adds support for package relay. If the mempool becomes congested and the feerate is too low, the commitment transaction may never reach miners' mempools because it's below the minimum relay feerate. +#### Public IP addresses can be DNS host names + +You can now specify a DNS host name as one of your `server.public-ips` addresses (see PR [#911](https://github.com/lightning/bolts/pull/911)). Note: you can not specify more than one DNS host name. + ## Verifying signatures You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it: diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index add6c41f59..632e9aee4a 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -332,6 +332,7 @@ eclair { use-for-ipv6 = true use-for-tor = true use-for-watchdogs = true + use-for-dnshostnames = true randomize-credentials = false // this allows tor stream isolation } 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 a3e7791e53..426b14fb6c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -31,11 +31,12 @@ import fr.acinq.eclair.io.MessageRelay.{NoRelay, RelayAll, RelayChannelsOnly, Re import fr.acinq.eclair.io.PeerConnection import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig import fr.acinq.eclair.payment.relay.Relayer.{RelayFees, RelayParams} +import fr.acinq.eclair.router.Announcements.AddressException import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios} import fr.acinq.eclair.router.PathFindingExperimentConf import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} import fr.acinq.eclair.tor.Socks5ProxyParams -import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress} +import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.ByteVector @@ -179,6 +180,7 @@ object NodeParams extends Logging { useForIPv6 = config.getBoolean("socks5.use-for-ipv6"), useForTor = config.getBoolean("socks5.use-for-tor"), useForWatchdogs = config.getBoolean("socks5.use-for-watchdogs"), + useForDnsHostnames = config.getBoolean("socks5.use-for-dnshostnames"), )) } else { None @@ -300,6 +302,19 @@ object NodeParams extends Logging { require(features.hasFeature(Features.ChannelType), s"${Features.ChannelType.rfcName} must be enabled") } + def validateAddresses(addresses: List[NodeAddress]): Unit = { + val addressesError = if (addresses.count(_.isInstanceOf[DnsHostname]) > 1) { + Some(AddressException(s"Invalid server.public-ip addresses: can not have more than one DNS host name.")) + } else { + addresses.collectFirst { + case address if address.isInstanceOf[Tor2] => AddressException(s"invalid server.public-ip address `$address`: Tor v2 is deprecated.") + case address if address.port == 0 && !address.isInstanceOf[Tor3] => AddressException(s"invalid server.public-ip address `$address`: A non-Tor address can not use port 0.") + } + } + + require(addressesError.isEmpty, addressesError.map(_.message)) + } + val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p } val features = Features.fromConfiguration(config.getConfig("features")) validateFeatures(features) @@ -332,6 +347,8 @@ object NodeParams extends Logging { .toList .map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ publicTorAddress_opt + validateAddresses(addresses) + val feeTargets = FeeTargets( fundingBlockTarget = config.getInt("on-chain-fees.target-blocks.funding"), commitmentBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment"), 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 3219d6f76f..1448d1df92 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,12 +44,13 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams], def receive: Receive = { case Symbol("connect") => - // note that there is no resolution here, it's either plain ip addresses, or unresolved tor hostnames + // note that only DNS host names are resolved here; plain ip addresses and tor hostnames are not resolved val remoteAddress = remoteNodeAddress match { case addr: IPv4 => new InetSocketAddress(addr.ipv4, addr.port) case addr: IPv6 => new InetSocketAddress(addr.ipv6, addr.port) case addr: Tor2 => InetSocketAddress.createUnresolved(addr.host, addr.port) case addr: Tor3 => InetSocketAddress.createUnresolved(addr.host, addr.port) + case addr: DnsHostname => new InetSocketAddress(addr.host, addr.port) } val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteNodeAddress, proxyParams))) match { case Some((proxyParams, Some(proxyAddress))) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala index a2c5602904..13f40fc235 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala @@ -211,7 +211,7 @@ object ReconnectionTask { } def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[NodeAddress] = { - val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toSeq.flatMap(_.addresses) + val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toList.flatMap(_.validAddresses) selectNodeAddress(nodeParams, nodeAddresses) } 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 82e752a702..334f58ca6f 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 @@ -70,11 +70,13 @@ object Announcements { def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = { require(alias.length <= 32) + // sort addresses by ascending address descriptor type; do not reorder addresses within the same descriptor type val sortedAddresses = nodeAddresses.map { case address@(_: IPv4) => (1, address) case address@(_: IPv6) => (2, address) case address@(_: Tor2) => (3, address) case address@(_: Tor3) => (4, address) + case address@(_: DnsHostname) => (5, address) }.sortBy(_._1).map(_._2) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty) val sig = Crypto.sign(witness, nodeSecret) @@ -89,6 +91,8 @@ object Announcements { ) } + case class AddressException(message: String) extends IllegalArgumentException(message) + /** * BOLT 7: * The creating node MUST set node-id-1 and node-id-2 to the public keys of the diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index b55356a422..2eee3188cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -253,6 +253,12 @@ object Validation { log.debug("received node announcement from {}", ctx.sender()) None } + val rebroadcastNode = if (n.shouldRebroadcast) { + Some(n -> origins) + } else { + log.debug("will not rebroadcast {}", n) + None + } if (d.stash.nodes.contains(n)) { log.debug("ignoring {} (already stashed)", n) val origins1 = d.stash.nodes(n) ++ origins @@ -275,13 +281,13 @@ object Validation { remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n))) ctx.system.eventStream.publish(NodeUpdated(n)) db.updateNode(n) - d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins))) + d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes ++ rebroadcastNode)) } else if (d.channels.values.exists(c => isRelatedTo(c.ann, n.nodeId))) { log.debug("added node nodeId={}", n.nodeId) remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n))) ctx.system.eventStream.publish(NodesDiscovered(n :: Nil)) db.addNode(n) - d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins))) + d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes ++ rebroadcastNode)) } else if (d.awaiting.keys.exists(c => isRelatedTo(c, n.nodeId))) { log.debug("stashing {}", n) d.copy(stash = d.stash.copy(nodes = d.stash.nodes + (n -> origins))) 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 0f27c104e7..3a0e1aba14 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 @@ -217,7 +217,7 @@ 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, useForWatchdogs: Boolean) +case class Socks5ProxyParams(address: InetSocketAddress, credentials_opt: Option[Credentials], randomizeCredentials: Boolean, useForIPv4: Boolean, useForIPv6: Boolean, useForTor: Boolean, useForWatchdogs: Boolean, useForDnsHostnames: Boolean) object Socks5ProxyParams { @@ -237,6 +237,7 @@ object Socks5ProxyParams { 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 _: DnsHostname if proxyParams.useForDnsHostnames => Some(proxyParams.address) case _ => None } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala index 314fbaf4bf..195534d3b4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala @@ -124,6 +124,7 @@ object CommonCodecs { .typecase(2, (ipv6address :: uint16).as[IPv6]) .typecase(3, (base32(10) :: uint16).as[Tor2]) .typecase(4, (base32(35) :: uint16).as[Tor3]) + .typecase(5, (variableSizeBytes(uint8, ascii) :: uint16).as[DnsHostname]) // 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/protocol/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala index 42e89d6e8d..4c82527d7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala @@ -351,16 +351,20 @@ object NodeAddress { /** * Creates a NodeAddress from a host and port. * - * Note that non-onion hosts will be resolved. + * Note that only IP v4 and v6 hosts will be resolved, onion and DNS hosts names will not 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. + * + * Host names that are not Tor, IPv4 or IPv6 are assumed to be a DNS name and are not immediately resolved. + * */ 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 _ => IPAddress(InetAddress.getByName(host), port) + case _ if InetAddresses.isInetAddress(host.filterNot(Set('[', ']'))) => IPAddress(InetAddress.getByName(host), port) + case _ => DnsHostname(host, port) } } @@ -387,6 +391,7 @@ case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv6) } case class Tor2(tor2: String, port: Int) extends OnionAddress { override def host: String = tor2 + ".onion" } case class Tor3(tor3: String, port: Int) extends OnionAddress { override def host: String = tor3 + ".onion" } +case class DnsHostname(dnsHostname: String, port: Int) extends IPAddress {override def host: String = dnsHostname} // @formatter:on case class NodeAnnouncement(signature: ByteVector64, @@ -396,7 +401,20 @@ case class NodeAnnouncement(signature: ByteVector64, rgbColor: Color, alias: String, addresses: List[NodeAddress], - tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp + tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp { + + val validAddresses: List[NodeAddress] = { + // if port is equal to 0, SHOULD ignore ipv6_addr OR ipv4_addr OR hostname; SHOULD ignore Tor v2 onion services. + val validAddresses = addresses.filter(address => address.port != 0 || address.isInstanceOf[Tor3]).filterNot(address => address.isInstanceOf[Tor2]) + // if more than one type 5 address is announced, SHOULD ignore the additional data. + validAddresses.filter(!_.isInstanceOf[DnsHostname]) ++ validAddresses.find(_.isInstanceOf[DnsHostname]) + } + + val shouldRebroadcast: Boolean = { + // if more than one type 5 address is announced, MUST not forward the node_announcement. + addresses.count(address => address.isInstanceOf[DnsHostname]) <= 1 + } +} case class ChannelUpdate(signature: ByteVector64, chainHash: ByteVector32, 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 c0779a845e..8e5a25e74f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -343,4 +343,28 @@ class StartupSpec extends AnyFunSuite { assert(nodeParamsAttempt2.isSuccess) } + test("NodeParams should fail when server.public-ips addresses or server.port are invalid") { + case class TestCase(publicIps: Seq[String], port: String, error: Option[String] = None, errorIp: Option[String] = None) + val testCases = Seq[TestCase]( + TestCase(Seq("0.0.0.0", "140.82.121.4", "2620:1ec:c11:0:0:0:0:200", "2620:1ec:c11:0:0:0:0:201", "iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion", "acinq.co"), "9735"), + TestCase(Seq("140.82.121.4", "2620:1ec:c11:0:0:0:0:200", "acinq.fr", "iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion"), "0", Some("port 0"), Some("140.82.121.4")), + TestCase(Seq("hsmithsxurybd7uh.onion", "iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion"), "9735", Some("Tor v2"), Some("hsmithsxurybd7uh.onion")), + TestCase(Seq("acinq.co", "acinq.fr"), "9735", Some("DNS host name")), + ) + testCases.foreach(test => { + val serverConf = ConfigFactory.parseMap(Map( + s"server.public-ips" -> test.publicIps.asJava, + s"server.port" -> test.port, + ).asJava).withFallback(defaultConf) + val attempt = Try(makeNodeParamsWithDefaults(serverConf)) + if (test.error.isEmpty) { + assert(attempt.isSuccess) + } else { + assert(attempt.isFailure) + assert(attempt.failed.get.getMessage.contains(test.error.get)) + assert(test.errorIp.isEmpty || attempt.failed.get.getMessage.contains(test.errorIp.get)) + } + }) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala index 819579ce96..be79abbeef 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/watchdogs/BlockchainWatchdogSpec.scala @@ -107,7 +107,8 @@ class BlockchainWatchdogSpec extends ScalaTestWithActorTestKit(ConfigFactory.loa useForIPv4 = true, useForIPv6 = true, useForTor = true, - useForWatchdogs = true) + useForWatchdogs = true, + useForDnsHostnames = true) if (proxyAcceptsConnections(proxyParams)) { val eventListener = TestProbe[DangerousBlocksSkew]() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala index 1af4ce6196..7805d5fbc6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/NetworkDbSpec.scala @@ -59,7 +59,8 @@ class NetworkDbSpec extends AnyFunSuite { val node_1 = Announcements.makeNodeAnnouncement(randomKey(), "node-alice", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features.empty) val node_2 = Announcements.makeNodeAnnouncement(randomKey(), "node-bob", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional)) val node_3 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), NodeAddress.fromParts("192.168.1.42", 42000).get :: Nil, Features(VariableLengthOnion -> Optional)) - val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor2("aaaqeayeaudaocaj", 42000) :: Nil, Features.empty) + val node_4 = Announcements.makeNodeAnnouncement(randomKey(), "node-eve", Color(100.toByte, 200.toByte, 300.toByte), Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000) :: Nil, Features.empty) + val node_5 = Announcements.makeNodeAnnouncement(randomKey(), "node-frank", Color(100.toByte, 200.toByte, 300.toByte), DnsHostname("eclair.invalid", 42000) :: Nil, Features.empty) assert(db.listNodes().toSet == Set.empty) db.addNode(node_1) @@ -69,12 +70,14 @@ class NetworkDbSpec extends AnyFunSuite { db.addNode(node_2) db.addNode(node_3) db.addNode(node_4) - assert(db.listNodes().toSet == Set(node_1, node_2, node_3, node_4)) + db.addNode(node_5) + assert(db.listNodes().toSet == Set(node_1, node_2, node_3, node_4, node_5)) db.removeNode(node_2.nodeId) - assert(db.listNodes().toSet == Set(node_1, node_3, node_4)) + assert(db.listNodes().toSet == Set(node_1, node_3, node_4, node_5)) db.updateNode(node_1) - assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) + assert(node_4.addresses == List(Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000))) + assert(node_5.addresses == List(DnsHostname("eclair.invalid", 42000))) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala index 949e24bdad..d8b60dc5a4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/NodeURISpec.scala @@ -37,8 +37,8 @@ class NodeURISpec extends AnyFunSuite { val testCases = List( TestCase(s"$PUBKEY@$IPV4_ENDURANCE:9737", IPV4_ENDURANCE, 9737), TestCase(s"$PUBKEY@$IPV4_ENDURANCE", IPV4_ENDURANCE, 9735), - TestCase(s"$PUBKEY@$NAME_ENDURANCE:9737", "13.248.222.197", 9737), - TestCase(s"$PUBKEY@$NAME_ENDURANCE", "13.248.222.197", 9735), + TestCase(s"$PUBKEY@$NAME_ENDURANCE:9737", NAME_ENDURANCE, 9737), + TestCase(s"$PUBKEY@$NAME_ENDURANCE", NAME_ENDURANCE, 9735), TestCase(s"$PUBKEY@$IPV6:9737", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9737), TestCase(s"$PUBKEY@$IPV6", "[2405:204:66a9:536c:873f:dc4a:f055:a298]", 9735), ) 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 99ec42ba8c..8fd1ce1c2e 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 @@ -137,6 +137,22 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle mockServer.close() } + test("return connection failure for a peer with an invalid dns host name") { f => + import f._ + + // this actor listens to connection requests and creates connections + system.actorOf(ClientSpawner.props(nodeParams.keyPair, nodeParams.socksProxy_opt, nodeParams.peerConnectionConf, TestProbe().ref, TestProbe().ref)) + + val invalidDnsHostname_opt = NodeAddress.fromParts("eclair.invalid", 9735).toOption + assert(invalidDnsHostname_opt.nonEmpty) + assert(invalidDnsHostname_opt.get == DnsHostname("eclair.invalid", 9735)) + + val probe = TestProbe() + probe.send(peer, Peer.Init(Set.empty)) + probe.send(peer, Peer.Connect(remoteNodeId, invalidDnsHostname_opt, probe.ref, isPersistent = true)) + probe.expectMsgType[PeerConnection.ConnectionResult.ConnectionFailed] + } + test("successfully reconnect to peer at startup when there are existing channels", Tag("auto_reconnect")) { f => import f._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala index f45044614f..de6cb9b8a1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/json/JsonSerializersSpec.scala @@ -95,12 +95,14 @@ class JsonSerializersSpec extends AnyFunSuite with Matchers { val ipv6LocalHost = IPAddress(InetAddress.getByAddress(Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)), 9735) val tor2 = Tor2("aaaqeayeaudaocaj", 7777) val tor3 = Tor3("aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc", 9999) + val dnsHostName = DnsHostname("acinq.co", 8888) JsonSerializers.serialization.write(ipv4)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""10.0.0.1:8888"""" JsonSerializers.serialization.write(ipv6)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[2405:204:66a9:536c:873f:dc4a:f055:a298]:9737"""" JsonSerializers.serialization.write(ipv6LocalHost)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""[::1]:9735"""" JsonSerializers.serialization.write(tor2)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocaj.onion:7777"""" JsonSerializers.serialization.write(tor3)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""aaaqeayeaudaocajbifqydiob4ibceqtcqkrmfyydenbwha5dypsaijc.onion:9999"""" + JsonSerializers.serialization.write(dnsHostName)(org.json4s.DefaultFormats + NodeAddressSerializer) shouldBe s""""acinq.co:8888"""" } test("PeerInfo serialization") { 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 b82360bc0f..f355c645b7 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 @@ -24,7 +24,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.router.Announcements._ import fr.acinq.eclair.wire.protocol.ChannelUpdate.ChannelFlags import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.nodeAnnouncementCodec -import fr.acinq.eclair.wire.protocol.{Color, NodeAddress} +import fr.acinq.eclair.wire.protocol.NodeAddress import org.scalatest.funsuite.AnyFunSuite import scodec.bits._ @@ -81,18 +81,46 @@ class AnnouncementsSpec extends AnyFunSuite { test("sort node announcement addresses") { val addresses = List( + NodeAddress.fromParts("acinq.co", 9735).get, NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, NodeAddress.fromParts("140.82.121.4", 9735).get, - NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, ) val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures()) assert(checkSig(ann)) assert(ann.addresses == List( NodeAddress.fromParts("140.82.121.4", 9735).get, NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, - NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, + NodeAddress.fromParts("acinq.co", 9735).get, + )) + } + + test("filter invalid and deprecated node announcement addresses") { + val addresses = List( + NodeAddress.fromParts("140.82.121.5", 9735).get, + NodeAddress.fromParts("140.82.121.4", 0).get, // ignore IPv4 with port 0 + NodeAddress.fromParts("140.82.121.4", 9735).get, // more than one IPv4 is OK + NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:201", 9735).get, + NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, // more than one IPv6 is OK + NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 0).get, // ignore IPv6 with port 0 + NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get, // deprecate Torv2 addresses + NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 0).get, // port zero for Tor is OK + NodeAddress.fromParts("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion", 9735).get, // more than one Tor is OK + NodeAddress.fromParts("acinq.co", 0).get, // ignore DNS hostname with port 0 + NodeAddress.fromParts("acinq.co", 9735).get, + NodeAddress.fromParts("acinq.fr", 9735).get, // ignore more than one DNS hostnames + ) + val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, addresses, Alice.nodeParams.features.nodeAnnouncementFeatures()) + assert(checkSig(ann)) + assert(ann.validAddresses === List( + NodeAddress.fromParts("140.82.121.5", 9735).get, + NodeAddress.fromParts("140.82.121.4", 9735).get, + NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:201", 9735).get, + NodeAddress.fromParts("2620:1ec:c11:0:0:0:0:200", 9735).get, + NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 0).get, + NodeAddress.fromParts("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion", 9735).get, + NodeAddress.fromParts("acinq.co", 9735).get )) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala index a7fe36fe11..d8a12ffdf5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/RouterSpec.scala @@ -747,4 +747,49 @@ class RouterSpec extends BaseRouterSpec { } } + test("properly announce valid new nodes announcements and ignore invalid ones") { fixture => + import fixture._ + val eventListener = TestProbe() + system.eventStream.subscribe(eventListener.ref, classOf[NetworkEvent]) + system.eventStream.subscribe(eventListener.ref, classOf[Rebroadcast]) + val peerConnection = TestProbe() + + { + // continue to rebroadcast node updates with deprecated Torv2 addresses + val torv2Address = List(NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get) + val node_c_torv2 = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), torv2Address, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 1) + peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_torv2)) + peerConnection.expectMsg(TransportHandler.ReadAck(node_c_torv2)) + peerConnection.expectMsg(GossipDecision.Accepted(node_c_torv2)) + eventListener.expectMsg(NodeUpdated(node_c_torv2)) + router ! Router.TickBroadcast + val rebroadcast = eventListener.expectMsgType[Rebroadcast] + assert(rebroadcast.nodes.contains(node_c_torv2)) + } + + { + // rebroadcast node updates with a single DNS hostname addresses + val hostname = List(NodeAddress.fromParts("acinq.co", 9735).get) + val node_c_hostname = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), hostname, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 10) + peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_hostname)) + peerConnection.expectMsg(TransportHandler.ReadAck(node_c_hostname)) + peerConnection.expectMsg(GossipDecision.Accepted(node_c_hostname)) + eventListener.expectMsg(NodeUpdated(node_c_hostname)) + router ! Router.TickBroadcast + val rebroadcast = eventListener.expectMsgType[Rebroadcast] + assert(rebroadcast.nodes.contains(node_c_hostname)) + } + + { + // do NOT rebroadcast node updates with more than one DNS hostname addresses + val multiHostnames = List(NodeAddress.fromParts("acinq.co", 9735).get, NodeAddress.fromParts("acinq.fr", 9735).get) + val node_c_noForward = makeNodeAnnouncement(priv_c, "node-C", Color(123, 100, -40), multiHostnames, TestConstants.Bob.nodeParams.features.nodeAnnouncementFeatures(), timestamp = TimestampSecond.now() + 20) + peerConnection.send(router, PeerRoutingMessage(peerConnection.ref, remoteNodeId, node_c_noForward)) + peerConnection.expectMsg(TransportHandler.ReadAck(node_c_noForward)) + peerConnection.expectMsg(GossipDecision.Accepted(node_c_noForward)) + eventListener.expectMsg(NodeUpdated(node_c_noForward)) + router ! Router.TickBroadcast + eventListener.expectNoMessage(100 millis) + } + } } 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 index a35cfb2b1f..6925b737a7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/tor/Socks5ConnectionSpec.scala @@ -17,9 +17,9 @@ package fr.acinq.eclair.tor import fr.acinq.eclair.wire.protocol.NodeAddress +import org.scalatest.funsuite.AnyFunSuite import java.net.InetSocketAddress -import org.scalatest.funsuite.AnyFunSuite /** * Created by PM on 27/01/2017. @@ -32,28 +32,36 @@ class Socks5ConnectionSpec extends AnyFunSuite { assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("1.2.3.4", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("1.2.3.4", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).isEmpty) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("[fc92:97a3:e057:b290:abd8:9bd6:135d:7e7]", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = false, useForTor = true, useForWatchdogs = true)).isEmpty) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = false, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).isEmpty) assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).contains(proxyAddress)) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).contains(proxyAddress)) assert(Socks5ProxyParams.proxyAddress( address = NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get, - proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = false, useForWatchdogs = true)).isEmpty) + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = false, useForWatchdogs = true, useForDnsHostnames = true)).isEmpty) + // DnsHostname "localhost" resolves to an IPv4 address + assert(Socks5ProxyParams.proxyAddress( + address = NodeAddress.fromParts("localhost", 9735).get, + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = true)).contains(proxyAddress)) + + assert(Socks5ProxyParams.proxyAddress( + address = NodeAddress.fromParts("localhost", 9735).get, + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = true, useForIPv6 = true, useForTor = true, useForWatchdogs = true, useForDnsHostnames = false)).isEmpty) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala index feeef78e46..af36908386 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/CommonCodecsSpec.scala @@ -210,6 +210,13 @@ class CommonCodecsSpec extends AnyFunSuite { val nodeaddr2 = nodeaddress.decode(bin).require.value assert(nodeaddr == nodeaddr2) } + { + val nodeaddr = DnsHostname("acinq.co", 4231) + val bin = nodeaddress.encode(nodeaddr).require + assert(bin === hex"05 086163696e712e636f 1087".toBitVector) + val nodeaddr2 = nodeaddress.decode(bin).require.value + assert(nodeaddr === nodeaddr2) + } } test("encode/decode bytevector32") { diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 1e88e1adcc..ff8a479a84 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -169,7 +169,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM ActorRef.noSender, nodeId = aliceNodeId, state = Peer.CONNECTED, - address = Some(NodeAddress.fromParts("localhost", 9731).get), + address = Some(NodeAddress.fromParts("127.0.0.1", 9731).get), channels = 1), PeerInfo( ActorRef.noSender, @@ -254,7 +254,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM chainHash = ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), network = "regtest", blockHeight = 9999, - publicAddresses = NodeAddress.fromParts("localhost", 9731).get :: Nil, + publicAddresses = NodeAddress.fromParts("127.0.0.1", 9731).get :: Nil, onionAddress = None, instanceId = "01234567-0123-4567-89ab-0123456789ab" ))