Skip to content

Commit 49846f5

Browse files
committed
Send remote address in init
This adds the option to report a remote IP address back to a connecting peer using the `init` message. A node can decide to use that information to discover a potential update to its public IP address (NAT) and use that for a `node_announcement` update message containing the new address. See lightning/bolts#917
1 parent 59b4035 commit 49846f5

File tree

6 files changed

+102
-29
lines changed

6 files changed

+102
-29
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co
5858

5959
val wireLog = new BusLogging(context.system.eventStream, "", classOf[Diagnostics], context.system.asInstanceOf[ExtendedActorSystem].logFilter) with DiagnosticLoggingAdapter
6060

61-
def diag(message: T, direction: String) = {
61+
def diag(message: T, direction: String): Unit = {
6262
require(direction == "IN" || direction == "OUT")
6363
val channelId_opt = Logs.channelId(message)
6464
wireLog.mdc(Logs.mdc(LogCategory(message), remoteNodeId_opt, channelId_opt))

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
101101
d.transport ! TransportHandler.Listener(self)
102102
Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment()
103103
log.info(s"using features=$localFeatures")
104-
val localInit = protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil)))
104+
val localInit = NodeAddress.extractPublicIPAddress(d.pendingAuth.address) match {
105+
case Some(remoteAddress) if !d.pendingAuth.outgoing => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress)))
106+
case _ => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil)))
107+
}
105108
d.transport ! localInit
106109
startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout)
107110
goto(INITIALIZING) using InitializingData(chainHash, d.pendingAuth, d.remoteNodeId, d.transport, peer, localInit, doSync)
@@ -114,6 +117,7 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A
114117
d.transport ! TransportHandler.ReadAck(remoteInit)
115118

116119
log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}")
120+
remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our public address is {}", address.socketAddress.toString))
117121

118122
val featureGraphErr_opt = Features.validateFeatureGraph(remoteInit.features)
119123
if (remoteInit.networks.nonEmpty && remoteInit.networks.intersect(d.localInit.networks).isEmpty) {

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

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
2121
import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Satoshi}
2222
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
2323
import fr.acinq.eclair.channel.ChannelType
24-
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64}
24+
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, TimestampSecond, UInt64}
2525
import scodec.bits.ByteVector
2626

2727
import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress}
@@ -49,6 +49,7 @@ sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <-
4949

5050
case class Init(features: Features, tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage {
5151
val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil)
52+
val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address)
5253
}
5354

5455
case class Warning(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[WarningTlv] = TlvStream.empty) extends SetupMessage with HasChannelId {
@@ -215,28 +216,48 @@ case class Color(r: Byte, g: Byte, b: Byte) {
215216
// @formatter:off
216217
sealed trait NodeAddress { def socketAddress: InetSocketAddress }
217218
sealed trait OnionAddress extends NodeAddress
219+
sealed trait IPAddress extends NodeAddress
220+
// @formatter:on
221+
218222
object NodeAddress {
219223
/**
220-
* Creates a NodeAddress from a host and port.
221-
*
222-
* Note that non-onion hosts will be resolved.
223-
*
224-
* We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on
225-
* the .onion TLD and rely on their length to separate v2/v3.
226-
*/
224+
* Creates a NodeAddress from a host and port.
225+
*
226+
* Note that non-onion hosts will be resolved.
227+
*
228+
* We don't attempt to resolve onion addresses (it will be done by the tor proxy), so we just recognize them based on
229+
* the .onion TLD and rely on their length to separate v2/v3.
230+
*/
227231
def fromParts(host: String, port: Int): Try[NodeAddress] = Try {
228232
host match {
229233
case _ if host.endsWith(".onion") && host.length == 22 => Tor2(host.dropRight(6), port)
230234
case _ if host.endsWith(".onion") && host.length == 62 => Tor3(host.dropRight(6), port)
231-
case _ => InetAddress.getByName(host) match {
235+
case _ => InetAddress.getByName(host) match {
232236
case a: Inet4Address => IPv4(a, port)
233237
case a: Inet6Address => IPv6(a, port)
234238
}
235239
}
236240
}
241+
242+
private def isPrivate(address: InetAddress): Boolean = address.isAnyLocalAddress || address.isLoopbackAddress || address.isLinkLocalAddress || address.isSiteLocalAddress
243+
244+
/**
245+
* Extract the public IP address from an incoming connection, when possible. This information can be sent back to our
246+
* peer to allow them to discover their public IP address from within their local network.
247+
*/
248+
def extractPublicIPAddress(socketAddress: InetSocketAddress): Option[IPAddress] = {
249+
val address = socketAddress.getAddress
250+
address match {
251+
case address: Inet4Address if !isPrivate(address) => Some(IPv4(address, socketAddress.getPort))
252+
case address: Inet6Address if !isPrivate(address) => Some(IPv6(address, socketAddress.getPort))
253+
case _ => None
254+
}
255+
}
237256
}
238-
case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv4, port) }
239-
case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv6, port) }
257+
258+
// @formatter:off
259+
case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv4, port) }
260+
case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv6, port) }
240261
case class Tor2(tor2: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) }
241262
case class Tor3(tor3: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) }
242263
// @formatter:on

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,24 @@ object InitTlv {
3535
/** The chains the node is interested in. */
3636
case class Networks(chainHashes: List[ByteVector32]) extends InitTlv
3737

38+
/**
39+
* When receiving an incoming connection, we can send back the public address our peer is connecting from.
40+
* This lets our peer discover if its public IP has changed from within its local network.
41+
*/
42+
case class RemoteAddress(address: NodeAddress) extends InitTlv
43+
3844
}
3945

4046
object InitTlvCodecs {
4147

4248
import InitTlv._
4349

4450
private val networks: Codec[Networks] = variableSizeBytesLong(varintoverflow, list(bytes32)).as[Networks]
51+
private val remoteAddress: Codec[RemoteAddress] = variableSizeBytesLong(varintoverflow, nodeaddress).as[RemoteAddress]
4552

4653
val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint)
4754
.typecase(UInt64(1), networks)
55+
.typecase(UInt64(3), remoteAddress)
4856
)
4957

5058
}

eclair-core/src/test/scala/fr/acinq/eclair/io/PeerConnectionSpec.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
9292
connect(nodeParams, remoteNodeId, switchboard, router, connection, transport, peerConnection, peer)
9393
}
9494

95+
test("send incoming connection's remote address in init") { f =>
96+
import f._
97+
val probe = TestProbe()
98+
val incomingConnection = PeerConnection.PendingAuth(connection.ref, None, fakeIPAddress.socketAddress, origin_opt = None, transport_opt = Some(transport.ref))
99+
assert(!incomingConnection.outgoing)
100+
probe.send(peerConnection, incomingConnection)
101+
transport.send(peerConnection, TransportHandler.HandshakeCompleted(remoteNodeId))
102+
switchboard.expectMsg(PeerConnection.Authenticated(peerConnection, remoteNodeId))
103+
probe.send(peerConnection, PeerConnection.InitializeConnection(peer.ref, nodeParams.chainHash, nodeParams.features, doSync = false))
104+
transport.expectMsgType[TransportHandler.Listener]
105+
val localInit = transport.expectMsgType[protocol.Init]
106+
assert(localInit.remoteAddress_opt === Some(fakeIPAddress))
107+
}
108+
95109
test("handle connection closed during authentication") { f =>
96110
import f._
97111
val probe = TestProbe()
@@ -372,5 +386,26 @@ class PeerConnectionSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wi
372386
assert(new String(warn2.data.toArray).startsWith("invalid announcement sig"))
373387
}
374388

389+
test("filter private IP addresses") { _ =>
390+
val testCases = Seq(
391+
NodeAddress.fromParts("127.0.0.1", 9735).get -> true,
392+
NodeAddress.fromParts("0.0.0.0", 9735).get -> true,
393+
NodeAddress.fromParts("192.168.0.1", 9735).get -> true,
394+
NodeAddress.fromParts("140.82.121.3", 9735).get -> false,
395+
NodeAddress.fromParts("0000:0000:0000:0000:0000:0000:0000:0001", 9735).get -> true,
396+
NodeAddress.fromParts("b643:8bb1:c1f9:0556:487c:0acb:2ba3:3cc2", 9735).get -> false,
397+
NodeAddress.fromParts("hsmithsxurybd7uh.onion", 9735).get -> true,
398+
NodeAddress.fromParts("iq7zhmhck54vcax2vlrdcavq2m32wao7ekh6jyeglmnuuvv3js57r4id.onion", 9735).get -> true,
399+
)
400+
for ((address, isPrivate) <- testCases) {
401+
val filtered = NodeAddress.extractPublicIPAddress(address.socketAddress)
402+
if (isPrivate) {
403+
assert(filtered === None)
404+
} else {
405+
assert(filtered === Some(address))
406+
}
407+
}
408+
}
409+
375410
}
376411

eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import org.scalatest.funsuite.AnyFunSuite
3030
import scodec.DecodeResult
3131
import scodec.bits.{BinStringSyntax, ByteVector, HexStringSyntax}
3232

33-
import java.net.{Inet4Address, InetAddress}
33+
import java.net.{Inet4Address, Inet6Address, InetAddress}
3434

3535
/**
3636
* Created by PM on 31/05/2016.
@@ -49,31 +49,36 @@ class LightningMessageCodecsSpec extends AnyFunSuite {
4949
def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey
5050

5151
test("encode/decode init message") {
52-
case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[ByteVector32], valid: Boolean, reEncoded: Option[ByteVector] = None)
52+
case class TestCase(encoded: ByteVector, rawFeatures: ByteVector, networks: List[ByteVector32], address: Option[IPAddress], valid: Boolean, reEncoded: Option[ByteVector] = None)
5353
val chainHash1 = ByteVector32(hex"0101010101010101010101010101010101010101010101010101010101010101")
5454
val chainHash2 = ByteVector32(hex"0202020202020202020202020202020202020202020202020202020202020202")
55+
val remoteAddress1 = IPv4(InetAddress.getByAddress(Array[Byte](140.toByte, 82.toByte, 121.toByte, 3.toByte)).asInstanceOf[Inet4Address], 9735)
56+
val remoteAddress2 = IPv6(InetAddress.getByAddress(hex"b643 8bb1 c1f9 0556 487c 0acb 2ba3 3cc2".toArray).asInstanceOf[Inet6Address], 9736)
5557
val testCases = Seq(
56-
TestCase(hex"0000 0000", hex"", Nil, valid = true), // no features
57-
TestCase(hex"0000 0002088a", hex"088a", Nil, valid = true), // no global features
58-
TestCase(hex"00020200 0000", hex"0200", Nil, valid = true, Some(hex"0000 00020200")), // no local features
59-
TestCase(hex"00020200 0002088a", hex"0a8a", Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - no conflict - same size
60-
TestCase(hex"00020200 0003020002", hex"020202", Nil, valid = true, Some(hex"0000 0003020202")), // local and global - no conflict - different sizes
61-
TestCase(hex"00020a02 0002088a", hex"0a8a", Nil, valid = true, Some(hex"0000 00020a8a")), // local and global - conflict - same size
62-
TestCase(hex"00022200 000302aaa2", hex"02aaa2", Nil, valid = true, Some(hex"0000 000302aaa2")), // local and global - conflict - different sizes
63-
TestCase(hex"0000 0002088a 03012a05022aa2", hex"088a", Nil, valid = true), // unknown odd records
64-
TestCase(hex"0000 0002088a 03012a04022aa2", hex"088a", Nil, valid = false), // unknown even records
65-
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101", hex"088a", Nil, valid = false), // invalid tlv stream
66-
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101", hex"088a", List(chainHash1), valid = true), // single network
67-
TestCase(hex"0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202", hex"088a", List(chainHash1, chainHash2), valid = true), // multiple networks
68-
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010103012a", hex"088a", List(chainHash1), valid = true), // network and unknown odd records
69-
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101010102012a", hex"088a", Nil, valid = false) // network and unknown even records
58+
TestCase(hex"0000 0000", hex"", Nil, None, valid = true), // no features
59+
TestCase(hex"0000 0002088a", hex"088a", Nil, None, valid = true), // no global features
60+
TestCase(hex"00020200 0000", hex"0200", Nil, None, valid = true, Some(hex"0000 00020200")), // no local features
61+
TestCase(hex"00020200 0002088a", hex"0a8a", Nil, None, valid = true, Some(hex"0000 00020a8a")), // local and global - no conflict - same size
62+
TestCase(hex"00020200 0003020002", hex"020202", Nil, None, valid = true, Some(hex"0000 0003020202")), // local and global - no conflict - different sizes
63+
TestCase(hex"00020a02 0002088a", hex"0a8a", Nil, None, valid = true, Some(hex"0000 00020a8a")), // local and global - conflict - same size
64+
TestCase(hex"00022200 000302aaa2", hex"02aaa2", Nil, None, valid = true, Some(hex"0000 000302aaa2")), // local and global - conflict - different sizes
65+
TestCase(hex"0000 0002088a 03012a05022aa2", hex"088a", Nil, None, valid = true), // unknown odd records
66+
TestCase(hex"0000 0002088a 03012a04022aa2", hex"088a", Nil, None, valid = false), // unknown even records
67+
TestCase(hex"0000 0002088a 0120010101010101010101010101010101010101010101010101010101010101", hex"088a", Nil, None, valid = false), // invalid tlv stream
68+
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101", hex"088a", List(chainHash1), None, valid = true), // single network
69+
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 0307018c5279032607", hex"088a", List(chainHash1), Some(remoteAddress1), valid = true), // single network and IPv4 address
70+
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 031302b6438bb1c1f90556487c0acb2ba33cc22608", hex"088a", List(chainHash1), Some(remoteAddress2), valid = true), // single network and IPv6 address
71+
TestCase(hex"0000 0002088a 014001010101010101010101010101010101010101010101010101010101010101010202020202020202020202020202020202020202020202020202020202020202", hex"088a", List(chainHash1, chainHash2), None, valid = true), // multiple networks
72+
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 c9012a", hex"088a", List(chainHash1), None, valid = true), // network and unknown odd records
73+
TestCase(hex"0000 0002088a 01200101010101010101010101010101010101010101010101010101010101010101 02012a", hex"088a", Nil, None, valid = false) // network and unknown even records
7074
)
7175

7276
for (testCase <- testCases) {
7377
if (testCase.valid) {
7478
val init = initCodec.decode(testCase.encoded.bits).require.value
7579
assert(init.features.toByteVector === testCase.rawFeatures)
7680
assert(init.networks === testCase.networks)
81+
assert(init.remoteAddress_opt === testCase.address)
7782
val encoded = initCodec.encode(init).require
7883
assert(encoded.bytes === testCase.reEncoded.getOrElse(testCase.encoded))
7984
assert(initCodec.decode(encoded).require.value === init)

0 commit comments

Comments
 (0)