diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md index 97bd4cc0e1..3ce006850b 100644 --- a/docs/release-notes/eclair-vnext.md +++ b/docs/release-notes/eclair-vnext.md @@ -38,6 +38,12 @@ Expired incoming invoices that are unpaid will be searched for and purged from t * `eclair.purge-expired-invoices.enabled = true * `eclair.purge-expired-invoices.interval = 24 hours` +#### Public IP addresses can be DNS host names, but not Tor v2 addresses + +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. + +Tor v2 addresses are no longer supported as a `server.public-ips` address and will be ignored in gossip messages (see PR [#940](https://github.com/lightning/bolts/pull/940]). + ## 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/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index ae01b21058..fb888ad9a0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -32,8 +32,8 @@ 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.Graph.{HeuristicsConstants, WeightRatios} -import fr.acinq.eclair.router.PathFindingExperimentConf import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries} +import fr.acinq.eclair.router.{Announcements, PathFindingExperimentConf} import fr.acinq.eclair.tor.Socks5ProxyParams import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress} import grizzled.slf4j.Logging @@ -297,6 +297,11 @@ object NodeParams extends Logging { require(features.hasFeature(Features.ChannelType), s"${Features.ChannelType.rfcName} must be enabled") } + def validateAddresses(addresses: List[NodeAddress]): Unit = { + val addressesError = Announcements.validateAddresses(addresses) + require(addressesError.isEmpty, addressesError.map(_.message)) + } + val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p } val features = Features.fromConfiguration(config.getConfig("features")) validateFeatures(features) @@ -328,6 +333,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 8946afeb54..f550e1b0e0 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 83b8930183..edc5a698fc 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,17 @@ object Announcements { ) } + case class AddressException(message: String) extends IllegalArgumentException(message) + + def validateAddresses(addresses: List[NodeAddress]): Option[AddressException] = { + 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.") + } + } + /** * 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 5f4d66c73c..8389cff596 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 @@ -206,6 +206,10 @@ 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 @@ -228,13 +232,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..7cf8fbf83f 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 @@ -237,6 +237,11 @@ 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 => InetAddress.getByName(address.host) match { + case _: Inet4Address if proxyParams.useForIPv4 => Some(proxyParams.address) + case _: Inet6Address if proxyParams.useForIPv6 => Some(proxyParams.address) + case _ => None + } 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 7e37e80478..9306ad5df8 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 @@ -126,12 +126,15 @@ object CommonCodecs { def base32(size: Int): Codec[String] = bytes(size).xmap(b => new Base32().encodeAsString(b.toArray).toLowerCase, a => ByteVector(new Base32().decode(a.toUpperCase()))) + val punycode: Codec[String] = variableSizeBytes(uint8, ascii) + val nodeaddress: Codec[NodeAddress] = discriminated[NodeAddress].by(uint8) .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]) + .typecase( 5, (punycode :: 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 6510657c48..ccd3143bdd 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 @@ -312,16 +312,22 @@ 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. + * + * We resolve host names comprised of only numbers and periods (IPv4) or that contain a colon (IPv6). + * Other host names are assumed to be a DNS name and are not immediately resolved. + * */ def fromParts(host: String, port: Int): Try[NodeAddress] = Try { + val ipv4v6 = "^([0-9.]*)?$|(:)".r 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 ipv4v6.findFirstIn(host).isDefined => IPAddress(InetAddress.getByName(host), port) + case _ => DnsHostname(host, port) } } @@ -348,6 +354,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, @@ -357,7 +364,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 { + + def 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.filter(_.isInstanceOf[DnsHostname]).take(1) + } + + def 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 fb7a86121f..31979d9902 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala @@ -273,4 +273,26 @@ 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 && attempt.failed.get.getMessage.contains(test.error.get) && + (test.errorIp.isEmpty || attempt.failed.get.getMessage.contains(test.errorIp.get))) + }) + } + } 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 bdc551e04b..2115045fb9 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 @@ -58,7 +58,7 @@ 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-charlie", Color(100.toByte, 200.toByte, 300.toByte), Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 42000) :: Nil, Features.empty) assert(db.listNodes().toSet === Set.empty) db.addNode(node_1) @@ -73,7 +73,7 @@ class NetworkDbSpec extends AnyFunSuite { assert(db.listNodes().toSet === Set(node_1, node_3, node_4)) db.updateNode(node_1) - assert(node_4.addresses == List(Tor2("aaaqeayeaudaocaj", 42000))) + assert(node_4.addresses == List(Tor3("of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad", 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 2fb1a4ade9..abe231d873 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 8f998f0a34..cd20cf2f98 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 @@ -136,6 +136,20 @@ 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 + + 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 1b3708bd4c..eb81a39e7b 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 b92e5f022e..4196a3d817 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 @@ -23,7 +23,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._ @@ -80,18 +80,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 a5fc1bb368..0af321437f 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 @@ -737,4 +737,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..5b3c02da68 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. @@ -54,6 +54,14 @@ class Socks5ConnectionSpec extends AnyFunSuite { 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) + // 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)).contains(proxyAddress)) + + assert(Socks5ProxyParams.proxyAddress( + address = NodeAddress.fromParts("localhost", 9735).get, + proxyParams = Socks5ProxyParams(address = proxyAddress, credentials_opt = None, randomizeCredentials = false, useForIPv4 = false, useForIPv6 = true, useForTor = true, useForWatchdogs = true)).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 dbc6b4e11e..c2bc7ca3ca 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") {