# Nim-LibP2P # Copyright (c) 2022 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) # at your option. # This file may not be copied, modified, or distributed except according to # those terms. import std/[sugar, tables, sequtils] import stew/results import pkg/[chronos, chronicles, metrics] import dial, peerid, peerinfo, multicodec, multistream, connmanager, stream/connection, transports/transport, nameresolving/nameresolver, upgrademngrs/upgrade, errors export dial, errors, results logScope: topics = "libp2p dialer" declareCounter(libp2p_total_dial_attempts, "total attempted dials") declareCounter(libp2p_successful_dials, "dialed successful peers") declareCounter(libp2p_failed_dials, "failed dials") declareCounter(libp2p_failed_upgrades_outgoing, "outgoing connections failed upgrades") type DialFailedError* = object of LPError Dialer* = ref object of Dial localPeerId*: PeerId ms: MultistreamSelect connManager: ConnManager dialLock: Table[PeerId, AsyncLock] transports: seq[Transport] nameResolver: NameResolver proc tryDial( peerId: Opt[PeerId], hostname: string, address: MultiAddress, transport: Transport): Future[Connection] {.async.} = trace "Dialing address", address, peerId, hostname result = try: libp2p_total_dial_attempts.inc() await transport.dial(hostname, address) except CancelledError as exc: debug "Dialing canceled", msg = exc.msg, peerId raise exc except CatchableError as exc: debug "Dialing failed", msg = exc.msg, peerId libp2p_failed_dials.inc() raise exc proc tryUpgrade( peerId: Opt[PeerId], hostname: string, transport: Transport, conn: Connection): Future[Connection] {.async.} = result = try: await transport.upgradeOutgoing(conn, peerId) except CatchableError as exc: # If we failed to upgrade the connection through one transport, # we won't succeeded through another - no use in trying again await conn.close() debug "Upgrade failed", msg = exc.msg, peerId if exc isnot CancelledError: libp2p_failed_upgrades_outgoing.inc() raise exc proc tryDialAndUpgrade( peerId: Opt[PeerId], hostname: string, address: MultiAddress, transport: Transport): Future[Connection] {.async.} = if transport.handles(address): # check if it can dial it let conn = await tryDial(peerId, hostname, address, transport) # also keep track of the connection's bottom unsafe transport direction # required by gossipsub scoring conn.transportDir = Direction.Out libp2p_successful_dials.inc() let upgradedConn = await tryUpgrade(peerId, hostname, transport, conn) doAssert not isNil(upgradedConn), "connection died after upgradeOutgoing" debug "Dial successful", upgradedConn, peerId = upgradedConn.peerId return upgradedConn raise newException(DialFailedError, "Transport does not handle the address") proc tryDialAndUpgrade( peerId: Opt[PeerId], hostname: string, address: MultiAddress, transports: seq[Transport]): Future[Connection] {.async.} = for transport in transports: try: return await tryDialAndUpgrade(peerId, hostname, address, transport) except CatchableError: continue raise newException(DialFailedError, "No transport succeeded") proc tryDialAndUpgrade( self: Dialer, peerId: Opt[PeerId], hostname: string, addresses: seq[MultiAddress]): Future[Connection] {.async.} = for address in addresses: try: return await tryDialAndUpgrade(peerId, hostname, address, self.transports) except CatchableError: continue raise newException(DialFailedError, "No address succeeded") proc expandDnsAddr( self: Dialer, peerId: Opt[PeerId], address: MultiAddress): Future[seq[(MultiAddress, Opt[PeerId])]] {.async.} = if not DNSADDR.matchPartial(address): return @[(address, peerId)] if isNil(self.nameResolver): info "Can't resolve DNSADDR without NameResolver", ma=address return @[] let toResolve = if peerId.isSome: address & MultiAddress.init(multiCodec("p2p"), peerId.tryGet()).tryGet() else: address resolved = await self.nameResolver.resolveDnsAddr(toResolve) for resolvedAddress in resolved: let lastPart = resolvedAddress[^1].tryGet() if lastPart.protoCode == Result[MultiCodec, string].ok(multiCodec("p2p")): let peerIdBytes = lastPart.protoArgument().tryGet() addrPeerId = PeerId.init(peerIdBytes).tryGet() result.add((resolvedAddress[0..^2].tryGet(), Opt.some(addrPeerId))) else: result.add((resolvedAddress, peerId)) proc dialAndUpgrade( self: Dialer, peerId: Opt[PeerId], addrs: seq[MultiAddress]): Future[Connection] {.async.} = debug "Dialing peer", peerId for rawAddress in addrs: # resolve potential dnsaddr let addresses = await self.expandDnsAddr(peerId, rawAddress) for (expandedAddress, addrPeerId) in addresses: # DNS resolution let hostname = expandedAddress.getHostname() resolvedAddresses = if isNil(self.nameResolver): @[expandedAddress] else: await self.nameResolver.resolveMAddress(expandedAddress) try: return await self.tryDialAndUpgrade(addrPeerId, hostname, resolvedAddresses) except CatchableError: continue proc internalConnect( self: Dialer, peerId: Opt[PeerId], addrs: seq[MultiAddress], forceDial: bool): Future[Connection] {.async.} = if Opt.some(self.localPeerId) == peerId: raise newException(CatchableError, "can't dial self!") # Ensure there's only one in-flight attempt per peer let lock = self.dialLock.mgetOrPut(peerId.get(default(PeerId)), newAsyncLock()) try: await lock.acquire() # Check if we have a connection already and try to reuse it var conn = if peerId.isSome: self.connManager.selectConn(peerId.get()) else: nil if conn != nil: if conn.atEof or conn.closed: # This connection should already have been removed from the connection # manager - it's essentially a bug that we end up here - we'll fail # for now, hoping that this will clean themselves up later... warn "dead connection in connection manager", conn await conn.close() raise newException(DialFailedError, "Zombie connection encountered") trace "Reusing existing connection", conn, direction = $conn.dir return conn let slot = self.connManager.getOutgoingSlot(forceDial) conn = try: await self.dialAndUpgrade(peerId, addrs) except CatchableError as exc: slot.release() raise exc slot.trackConnection(conn) if isNil(conn): # None of the addresses connected raise newException(DialFailedError, "Unable to establish outgoing link") # A disconnect could have happened right after # we've added the connection so we check again # to prevent races due to that. if conn.closed() or conn.atEof(): # This can happen when the other ends drops us # before we get a chance to return the connection # back to the dialer. trace "Connection dead on arrival", conn raise newLPStreamClosedError() return conn finally: if lock.locked(): lock.release() method connect*( self: Dialer, peerId: PeerId, addrs: seq[MultiAddress], forceDial = false) {.async.} = ## connect remote peer without negotiating ## a protocol ## if self.connManager.connCount(peerId) > 0: return discard await self.internalConnect(Opt.some(peerId), addrs, forceDial) method connect*( self: Dialer, address: MultiAddress, allowUnknownPeerId = false): Future[PeerId] {.async.} = ## Connects to a peer and retrieve its PeerId let fullAddress = parseFullAddress(address) if fullAddress.isOk: return (await self.internalConnect( Opt.some(fullAddress.get()[0]), @[fullAddress.get()[1]], false)).peerId else: if allowUnknownPeerId == false: raise newException(DialFailedError, "Address without PeerID and unknown peer id disabled!") return (await self.internalConnect( Opt.none(PeerId), @[address], false)).peerId proc negotiateStream( self: Dialer, conn: Connection, protos: seq[string]): Future[Connection] {.async.} = trace "Negotiating stream", conn, protos let selected = await self.ms.select(conn, protos) if not protos.contains(selected): await conn.closeWithEOF() raise newException(DialFailedError, "Unable to select sub-protocol " & $protos) return conn method tryDial*( self: Dialer, peerId: PeerId, addrs: seq[MultiAddress]): Future[Opt[MultiAddress]] {.async.} = ## Create a protocol stream in order to check ## if a connection is possible. ## Doesn't use the Connection Manager to save it. ## trace "Check if it can dial", peerId, addrs try: let conn = await self.dialAndUpgrade(Opt.some(peerId), addrs) if conn.isNil(): raise newException(DialFailedError, "No valid multiaddress") await conn.close() return conn.observedAddr except CancelledError as exc: raise exc except CatchableError as exc: raise newException(DialFailedError, exc.msg) method dial*( self: Dialer, peerId: PeerId, protos: seq[string]): Future[Connection] {.async.} = ## create a protocol stream over an ## existing connection ## trace "Dialing (existing)", peerId, protos let stream = await self.connManager.getStream(peerId) if stream.isNil: raise newException(DialFailedError, "Couldn't get muxed stream") return await self.negotiateStream(stream, protos) method dial*( self: Dialer, peerId: PeerId, addrs: seq[MultiAddress], protos: seq[string], forceDial = false): Future[Connection] {.async.} = ## create a protocol stream and establish ## a connection if one doesn't exist already ## var conn: Connection stream: Connection proc cleanup() {.async.} = if not(isNil(stream)): await stream.closeWithEOF() if not(isNil(conn)): await conn.close() try: trace "Dialing (new)", peerId, protos conn = await self.internalConnect(Opt.some(peerId), addrs, forceDial) trace "Opening stream", conn stream = await self.connManager.getStream(conn) if isNil(stream): raise newException(DialFailedError, "Couldn't get muxed stream") return await self.negotiateStream(stream, protos) except CancelledError as exc: trace "Dial canceled", conn await cleanup() raise exc except CatchableError as exc: debug "Error dialing", conn, msg = exc.msg await cleanup() raise exc method addTransport*(self: Dialer, t: Transport) = self.transports &= t proc new*( T: type Dialer, localPeerId: PeerId, connManager: ConnManager, transports: seq[Transport], ms: MultistreamSelect, nameResolver: NameResolver = nil): Dialer = T(localPeerId: localPeerId, connManager: connManager, transports: transports, ms: ms, nameResolver: nameResolver)