diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala index 3380308b65..1a333c8c78 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/TransportHandler.scala @@ -58,7 +58,7 @@ class TransportHandler[T: ClassTag](keyPair: KeyPair, rs: Option[ByteVector], co val wireLog = new BusLogging(context.system.eventStream, "", classOf[Diagnostics], context.system.asInstanceOf[ExtendedActorSystem].logFilter) with DiagnosticLoggingAdapter - def diag(message: T, direction: String) = { + def diag(message: T, direction: String): Unit = { require(direction == "IN" || direction == "OUT") val channelId_opt = Logs.channelId(message) wireLog.mdc(Logs.mdc(LogCategory(message), remoteNodeId_opt, channelId_opt)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala index 8fe812ffe7..517c12d4b9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerConnection.scala @@ -101,7 +101,10 @@ class PeerConnection(keyPair: KeyPair, conf: PeerConnection.Conf, switchboard: A d.transport ! TransportHandler.Listener(self) Metrics.PeerConnectionsConnecting.withTag(Tags.ConnectionState, Tags.ConnectionStates.Initializing).increment() log.info(s"using features=$localFeatures") - val localInit = protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil))) + val localInit = NodeAddress.extractPublicIPAddress(d.pendingAuth.address) match { + case Some(remoteAddress) if !d.pendingAuth.outgoing => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil), InitTlv.RemoteAddress(remoteAddress))) + case None => protocol.Init(localFeatures, TlvStream(InitTlv.Networks(chainHash :: Nil))) + } d.transport ! localInit startSingleTimer(INIT_TIMER, InitTimeout, conf.initTimeout) 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 d.transport ! TransportHandler.ReadAck(remoteInit) log.info(s"peer is using features=${remoteInit.features}, networks=${remoteInit.networks.mkString(",")}") + remoteInit.remoteAddress_opt.foreach(address => log.info("peer reports that our public address is {}", address.socketAddress.toString)) val featureGraphErr_opt = Features.validateFeatureGraph(remoteInit.features) if (remoteInit.networks.nonEmpty && remoteInit.networks.intersect(d.localInit.networks).isEmpty) { 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 f5a1775e4f..5f5bf80e2d 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 @@ -49,6 +49,7 @@ sealed trait HtlcSettlementMessage extends UpdateMessage { def id: Long } // <- case class Init(features: Features, tlvStream: TlvStream[InitTlv] = TlvStream.empty) extends SetupMessage { val networks = tlvStream.get[InitTlv.Networks].map(_.chainHashes).getOrElse(Nil) + val remoteAddress_opt = tlvStream.get[InitTlv.RemoteAddress].map(_.address) } case class Warning(channelId: ByteVector32, data: ByteVector, tlvStream: TlvStream[WarningTlv] = TlvStream.empty) extends SetupMessage with HasChannelId { @@ -215,28 +216,47 @@ case class Color(r: Byte, g: Byte, b: Byte) { // @formatter:off sealed trait NodeAddress { def socketAddress: InetSocketAddress } sealed trait OnionAddress extends NodeAddress +sealed trait IPAddress extends NodeAddress +// @formatter:on + object NodeAddress { /** - * Creates a NodeAddress from a host and port. - * - * Note that non-onion hosts will 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. - */ + * Creates a NodeAddress from a host and port. + * + * Note that non-onion hosts will 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. + */ 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 _ => InetAddress.getByName(host) match { + case _ => InetAddress.getByName(host) match { case a: Inet4Address => IPv4(a, port) case a: Inet6Address => IPv6(a, port) } } } + + /** + * Extract the public IP address from an incoming connection, when possible. This information can be sent back to our + * peer to allow them to discover their public IP address from within their local network. + */ + def extractPublicIPAddress(socketAddress: InetSocketAddress): Option[IPAddress] = { + val address = socketAddress.getAddress + val isPrivate = address.isAnyLocalAddress || address.isLoopbackAddress || address.isLinkLocalAddress || address.isSiteLocalAddress + address match { + case address: Inet4Address if !isPrivate => Some(IPv4(address, socketAddress.getPort)) + case address: Inet6Address if !isPrivate => Some(IPv6(address, socketAddress.getPort)) + case _ => None + } + } } -case class IPv4(ipv4: Inet4Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv4, port) } -case class IPv6(ipv6: Inet6Address, port: Int) extends NodeAddress { override def socketAddress = new InetSocketAddress(ipv6, port) } + +// @formatter:off +case class IPv4(ipv4: Inet4Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv4, port) } +case class IPv6(ipv6: Inet6Address, port: Int) extends IPAddress { override def socketAddress = new InetSocketAddress(ipv6, port) } case class Tor2(tor2: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor2 + ".onion", port) } case class Tor3(tor3: String, port: Int) extends OnionAddress { override def socketAddress = InetSocketAddress.createUnresolved(tor3 + ".onion", port) } // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala index ebcb8a9252..881077fdbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/SetupAndControlTlv.scala @@ -35,6 +35,12 @@ object InitTlv { /** The chains the node is interested in. */ case class Networks(chainHashes: List[ByteVector32]) extends InitTlv + /** + * When receiving an incoming connection, we can send back the public address our peer is connecting from. + * This lets our peer discover if its public IP has changed from within its local network. + */ + case class RemoteAddress(address: NodeAddress) extends InitTlv + } object InitTlvCodecs { @@ -42,9 +48,11 @@ object InitTlvCodecs { import InitTlv._ private val networks: Codec[Networks] = variableSizeBytesLong(varintoverflow, list(bytes32)).as[Networks] + private val remoteAddress: Codec[RemoteAddress] = variableSizeBytesLong(varintoverflow, nodeaddress).as[RemoteAddress] val initTlvCodec = tlvStream(discriminated[InitTlv].by(varint) .typecase(UInt64(1), networks) + .typecase(UInt64(3), remoteAddress) ) }