Skip to content

Commit 2db6978

Browse files
committed
Support DNS hostnames and deprecate Torv2 addresses in node announcements
1 parent 7883bf6 commit 2db6978

File tree

17 files changed

+200
-16
lines changed

17 files changed

+200
-16
lines changed

docs/release-notes/eclair-vnext.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ Expired incoming invoices that are unpaid will be searched for and purged from t
3737
* `eclair.purge-expired-invoices.enabled = true
3838
* `eclair.purge-expired-invoices.interval = 24 hours`
3939

40+
#### Public IP addresses can be DNS host names, but not Tor v2 addresses
41+
42+
You can now specify a DNS host names 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.
43+
44+
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]).
45+
4046
## Verifying signatures
4147

4248
You will need `gpg` and our release signing key 7A73FE77DE2C4027. Note that you can get it:

eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import fr.acinq.eclair.io.PeerConnection
3232
import fr.acinq.eclair.message.OnionMessages.OnionMessageConfig
3333
import fr.acinq.eclair.payment.relay.Relayer.{RelayFees, RelayParams}
3434
import fr.acinq.eclair.router.Graph.{HeuristicsConstants, WeightRatios}
35-
import fr.acinq.eclair.router.PathFindingExperimentConf
3635
import fr.acinq.eclair.router.Router.{MultiPartParams, PathFindingConf, RouterConf, SearchBoundaries}
36+
import fr.acinq.eclair.router.{Announcements, PathFindingExperimentConf}
3737
import fr.acinq.eclair.tor.Socks5ProxyParams
3838
import fr.acinq.eclair.wire.protocol.{Color, EncodingType, NodeAddress}
3939
import grizzled.slf4j.Logging
@@ -297,6 +297,11 @@ object NodeParams extends Logging {
297297
require(features.hasFeature(Features.ChannelType), s"${Features.ChannelType.rfcName} must be enabled")
298298
}
299299

300+
def validateAddresses(addresses: List[NodeAddress]): Unit = {
301+
val addressesError = Announcements.validateAddresses(addresses)
302+
require(addressesError.isEmpty, addressesError.map(_.message))
303+
}
304+
300305
val pluginMessageParams = pluginParams.collect { case p: CustomFeaturePlugin => p }
301306
val features = Features.fromConfiguration(config.getConfig("features"))
302307
validateFeatures(features)
@@ -328,6 +333,8 @@ object NodeParams extends Logging {
328333
.toList
329334
.map(ip => NodeAddress.fromParts(ip, config.getInt("server.port")).get) ++ publicTorAddress_opt
330335

336+
validateAddresses(addresses)
337+
331338
val feeTargets = FeeTargets(
332339
fundingBlockTarget = config.getInt("on-chain-fees.target-blocks.funding"),
333340
commitmentBlockTarget = config.getInt("on-chain-fees.target-blocks.commitment"),

eclair-core/src/main/scala/fr/acinq/eclair/io/Client.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ class Client(keyPair: KeyPair, socks5ProxyParams_opt: Option[Socks5ProxyParams],
4444

4545
def receive: Receive = {
4646
case Symbol("connect") =>
47-
// note that there is no resolution here, it's either plain ip addresses, or unresolved tor hostnames
47+
// note that only DNS host names are resolved here; plain ip addresses and tor hostnames are not resolved
4848
val remoteAddress = remoteNodeAddress match {
4949
case addr: IPv4 => new InetSocketAddress(addr.ipv4, addr.port)
5050
case addr: IPv6 => new InetSocketAddress(addr.ipv6, addr.port)
5151
case addr: Tor2 => InetSocketAddress.createUnresolved(addr.host, addr.port)
5252
case addr: Tor3 => InetSocketAddress.createUnresolved(addr.host, addr.port)
53+
case addr: DnsHostname => new InetSocketAddress(addr.host, addr.port)
5354
}
5455
val (peerOrProxyAddress, proxyParams_opt) = socks5ProxyParams_opt.map(proxyParams => (proxyParams, Socks5ProxyParams.proxyAddress(remoteNodeAddress, proxyParams))) match {
5556
case Some((proxyParams, Some(proxyAddress))) =>

eclair-core/src/main/scala/fr/acinq/eclair/io/ReconnectionTask.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ object ReconnectionTask {
211211
}
212212

213213
def getPeerAddressFromDb(nodeParams: NodeParams, remoteNodeId: PublicKey): Option[NodeAddress] = {
214-
val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toSeq.flatMap(_.addresses)
214+
val nodeAddresses = nodeParams.db.peers.getPeer(remoteNodeId).toSeq ++ nodeParams.db.network.getNode(remoteNodeId).toList.flatMap(_.validAddresses)
215215
selectNodeAddress(nodeParams, nodeAddresses)
216216
}
217217

eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ object Announcements {
7070

7171
def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], features: Features[NodeFeature], timestamp: TimestampSecond = TimestampSecond.now()): NodeAnnouncement = {
7272
require(alias.length <= 32)
73+
// sort addresses by ascending address descriptor type; do not reorder addresses within the same descriptor type
7374
val sortedAddresses = nodeAddresses.map {
7475
case address@(_: IPv4) => (1, address)
7576
case address@(_: IPv6) => (2, address)
7677
case address@(_: Tor2) => (3, address)
7778
case address@(_: Tor3) => (4, address)
79+
case address@(_: DnsHostname) => (5, address)
7880
}.sortBy(_._1).map(_._2)
7981
val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features.unscoped(), sortedAddresses, TlvStream.empty)
8082
val sig = Crypto.sign(witness, nodeSecret)
@@ -89,6 +91,17 @@ object Announcements {
8991
)
9092
}
9193

94+
case class AddressException(message: String) extends IllegalArgumentException(message)
95+
96+
def validateAddresses(addresses: List[NodeAddress]): Option[AddressException] = {
97+
if (addresses.count(_.isInstanceOf[DnsHostname]) > 1)
98+
Some(AddressException(s"Invalid server.public-ip addresses: can not have more than one DNS host name."))
99+
else addresses.collectFirst {
100+
case address if address.isInstanceOf[Tor2] => AddressException(s"invalid server.public-ip address `$address`: Tor v2 is deprecated.")
101+
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.")
102+
}
103+
}
104+
92105
/**
93106
* BOLT 7:
94107
* The creating node MUST set node-id-1 and node-id-2 to the public keys of the

eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ object Validation {
206206
log.debug("received node announcement from {}", ctx.sender())
207207
None
208208
}
209+
val rebroadcastNode = if (n.shouldRebroadcast) Some(n -> origins) else {
210+
log.debug("will not rebroadcast {}", n)
211+
None
212+
}
209213
if (d.stash.nodes.contains(n)) {
210214
log.debug("ignoring {} (already stashed)", n)
211215
val origins1 = d.stash.nodes(n) ++ origins
@@ -228,13 +232,12 @@ object Validation {
228232
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n)))
229233
ctx.system.eventStream.publish(NodeUpdated(n))
230234
db.updateNode(n)
231-
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)))
235+
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes ++ rebroadcastNode))
232236
} else if (d.channels.values.exists(c => isRelatedTo(c.ann, n.nodeId))) {
233237
log.debug("added node nodeId={}", n.nodeId)
234238
remoteOrigins.foreach(sendDecision(_, GossipDecision.Accepted(n)))
235239
ctx.system.eventStream.publish(NodesDiscovered(n :: Nil))
236-
db.addNode(n)
237-
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes + (n -> origins)))
240+
d.copy(nodes = d.nodes + (n.nodeId -> n), rebroadcast = d.rebroadcast.copy(nodes = d.rebroadcast.nodes ++ rebroadcastNode))
238241
} else if (d.awaiting.keys.exists(c => isRelatedTo(c, n.nodeId))) {
239242
log.debug("stashing {}", n)
240243
d.copy(stash = d.stash.copy(nodes = d.stash.nodes + (n -> origins)))

eclair-core/src/main/scala/fr/acinq/eclair/tor/Socks5Connection.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ object Socks5ProxyParams {
237237
case _: IPv6 if proxyParams.useForIPv6 => Some(proxyParams.address)
238238
case _: Tor2 if proxyParams.useForTor => Some(proxyParams.address)
239239
case _: Tor3 if proxyParams.useForTor => Some(proxyParams.address)
240+
case _: DnsHostname => InetAddress.getByName(address.host) match {
241+
case _: Inet4Address if proxyParams.useForIPv4 => Some(proxyParams.address)
242+
case _: Inet6Address if proxyParams.useForIPv6 => Some(proxyParams.address)
243+
case _ => None
244+
}
240245
case _ => None
241246
}
242247

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/CommonCodecs.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,15 @@ object CommonCodecs {
118118

119119
def base32(size: Int): Codec[String] = bytes(size).xmap(b => new Base32().encodeAsString(b.toArray).toLowerCase, a => ByteVector(new Base32().decode(a.toUpperCase())))
120120

121+
val punycode: Codec[String] = variableSizeBytes(uint8, ascii)
122+
121123
val nodeaddress: Codec[NodeAddress] =
122124
discriminated[NodeAddress].by(uint8)
123125
.typecase(1, (ipv4address :: uint16).as[IPv4])
124126
.typecase(2, (ipv6address :: uint16).as[IPv6])
125127
.typecase(3, (base32(10) :: uint16).as[Tor2])
126128
.typecase(4, (base32(35) :: uint16).as[Tor3])
129+
.typecase( 5, (punycode :: uint16).as[DnsHostname])
127130

128131
// this one is a bit different from most other codecs: the first 'len' element is *not* the number of items
129132
// in the list but rather the number of bytes of the encoded list. The rationale is once we've read this

eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/LightningMessageTypes.scala

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,16 +224,22 @@ object NodeAddress {
224224
/**
225225
* Creates a NodeAddress from a host and port.
226226
*
227-
* Note that non-onion hosts will be resolved.
227+
* Note that only IP v4 and v6 hosts will be resolved, onion and DNS hosts names will not be resolved.
228228
*
229229
* We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on
230230
* the .onion TLD and rely on their length to separate v2/v3.
231+
*
232+
* We resolve host names comprised of only numbers and periods (IPv4) or that contain a colon (IPv6).
233+
* Other host names are assumed to be a DNS name and are not immediately resolved.
234+
*
231235
*/
232236
def fromParts(host: String, port: Int): Try[NodeAddress] = Try {
237+
val ipv4v6 = "^([0-9.]*)?$|(:)".r
233238
host match {
234239
case _ if host.endsWith(".onion") && host.length == 22 => Tor2(host.dropRight(6), port)
235240
case _ if host.endsWith(".onion") && host.length == 62 => Tor3(host.dropRight(6), port)
236-
case _ => IPAddress(InetAddress.getByName(host), port)
241+
case _ if ipv4v6.findFirstIn(host).isDefined => IPAddress(InetAddress.getByName(host), port)
242+
case _ => DnsHostname(host, port)
237243
}
238244
}
239245

@@ -260,6 +266,7 @@ case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def
260266
case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def host: String = InetAddresses.toUriString(ipv6) }
261267
case class Tor2(tor2: String, port: Int) extends OnionAddress { override def host: String = tor2 + ".onion" }
262268
case class Tor3(tor3: String, port: Int) extends OnionAddress { override def host: String = tor3 + ".onion" }
269+
case class DnsHostname(dnsHostname: String, port: Int) extends IPAddress {override def host: String = dnsHostname}
263270
// @formatter:on
264271

265272
case class NodeAnnouncement(signature: ByteVector64,
@@ -269,7 +276,20 @@ case class NodeAnnouncement(signature: ByteVector64,
269276
rgbColor: Color,
270277
alias: String,
271278
addresses: List[NodeAddress],
272-
tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp
279+
tlvStream: TlvStream[NodeAnnouncementTlv] = TlvStream.empty) extends RoutingMessage with AnnouncementMessage with HasTimestamp {
280+
281+
def validAddresses: List[NodeAddress] = {
282+
// if port is equal to 0, SHOULD ignore ipv6_addr OR ipv4_addr OR hostname; SHOULD ignore Tor v2 onion services.
283+
val validAddresses = addresses.filter(address => address.port != 0 || address.isInstanceOf[Tor3]).filterNot( address => address.isInstanceOf[Tor2])
284+
// if more than one type 5 address is announced, SHOULD ignore the additional data.
285+
validAddresses.filter(!_.isInstanceOf[DnsHostname]) ++ validAddresses.filter(_.isInstanceOf[DnsHostname]).take(1)
286+
}
287+
288+
def shouldRebroadcast: Boolean = {
289+
// if more than one type 5 address is announced, MUST not forward the node_announcement.
290+
addresses.count(address => address.isInstanceOf[DnsHostname]) <= 1
291+
}
292+
}
273293

274294
case class ChannelUpdate(signature: ByteVector64,
275295
chainHash: ByteVector32,

eclair-core/src/test/scala/fr/acinq/eclair/StartupSpec.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,26 @@ class StartupSpec extends AnyFunSuite {
273273
assert(nodeParamsAttempt2.isSuccess)
274274
}
275275

276+
test("NodeParams should fail when server.public-ips addresses or server.port are invalid") {
277+
case class TestCase(publicIps: Seq[String], port: String, error: Option[String] = None, errorIp: Option[String] = None)
278+
val testCases = Seq[TestCase](
279+
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"),
280+
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")),
281+
TestCase(Seq("hsmithsxurybd7uh.onion", "iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion"), "9735", Some("Tor v2"), Some("hsmithsxurybd7uh.onion")),
282+
TestCase(Seq("acinq.co", "acinq.fr"), "9735", Some("DNS host name")),
283+
)
284+
testCases.foreach( test => {
285+
val serverConf = ConfigFactory.parseMap(Map(
286+
s"server.public-ips" -> test.publicIps.asJava,
287+
s"server.port" -> test.port,
288+
).asJava).withFallback(defaultConf)
289+
val attempt = Try(makeNodeParamsWithDefaults(serverConf))
290+
if (test.error.isEmpty)
291+
assert(attempt.isSuccess)
292+
else
293+
assert(attempt.isFailure && attempt.failed.get.getMessage.contains(test.error.get) &&
294+
(test.errorIp.isEmpty || attempt.failed.get.getMessage.contains(test.errorIp.get)))
295+
})
296+
}
297+
276298
}

0 commit comments

Comments
 (0)