libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
540 lines (447 loc) • 18.7 kB
text/typescript
/* eslint-disable max-depth */
import { TimeoutError, DialError, AbortError } from '@libp2p/interface'
import { PeerMap } from '@libp2p/peer-collections'
import { PriorityQueue } from '@libp2p/utils/priority-queue'
import { multiaddr } from '@multiformats/multiaddr'
import { Circuit } from '@multiformats/multiaddr-matcher'
import { anySignal } from 'any-signal'
import { setMaxListeners } from 'main-event'
import { CustomProgressEvent } from 'progress-events'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { DialDeniedError, NoValidAddressesError } from '../errors.js'
import { getPeerAddress } from '../get-peer.js'
import { defaultAddressSorter } from './address-sorter.js'
import {
DIAL_TIMEOUT,
MAX_PARALLEL_DIALS,
MAX_PEER_ADDRS_TO_DIAL,
LAST_DIAL_FAILURE_KEY,
MAX_DIAL_QUEUE_LENGTH,
LAST_DIAL_SUCCESS_KEY
} from './constants.js'
import { resolveMultiaddr, dnsaddrResolver } from './resolvers/index.js'
import { DEFAULT_DIAL_PRIORITY } from './index.js'
import type { AddressSorter, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting, IsDialableOptions, OpenConnectionProgressEvents, MultiaddrResolver } from '@libp2p/interface'
import type { OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal'
import type { PriorityQueueJobOptions } from '@libp2p/utils/priority-queue'
import type { DNS } from '@multiformats/dns'
import type { Multiaddr } from '@multiformats/multiaddr'
import type { ProgressOptions } from 'progress-events'
export interface PendingDialTarget {
resolve(value: any): void
reject(err: Error): void
}
interface DialQueueJobOptions extends PriorityQueueJobOptions, ProgressOptions<OpenConnectionProgressEvents> {
peerId?: PeerId
multiaddrs: Set<string>
}
interface DialerInit {
addressSorter?: AddressSorter
maxParallelDials?: number
maxDialQueueLength?: number
maxPeerAddrsToDial?: number
dialTimeout?: number
resolvers?: Record<string, MultiaddrResolver>
connections?: PeerMap<Connection[]>
}
const defaultOptions = {
maxParallelDials: MAX_PARALLEL_DIALS,
maxDialQueueLength: MAX_DIAL_QUEUE_LENGTH,
maxPeerAddrsToDial: MAX_PEER_ADDRS_TO_DIAL,
dialTimeout: DIAL_TIMEOUT,
resolvers: {
dnsaddr: dnsaddrResolver
}
}
interface DialQueueComponents {
peerId: PeerId
metrics?: Metrics
peerStore: PeerStore
peerRouting: PeerRouting
transportManager: TransportManager
connectionGater: ConnectionGater
logger: ComponentLogger
dns?: DNS
}
export class DialQueue {
public queue: PriorityQueue<Connection, DialQueueJobOptions>
private readonly components: DialQueueComponents
private readonly addressSorter?: AddressSorter
private readonly maxPeerAddrsToDial: number
private readonly maxDialQueueLength: number
private readonly dialTimeout: number
private shutDownController: AbortController
private readonly connections: PeerMap<Connection[]>
private readonly log: Logger
private readonly resolvers: Record<string, MultiaddrResolver>
constructor (components: DialQueueComponents, init: DialerInit = {}) {
this.addressSorter = init.addressSorter
this.maxPeerAddrsToDial = init.maxPeerAddrsToDial ?? defaultOptions.maxPeerAddrsToDial
this.maxDialQueueLength = init.maxDialQueueLength ?? defaultOptions.maxDialQueueLength
this.dialTimeout = init.dialTimeout ?? defaultOptions.dialTimeout
this.connections = init.connections ?? new PeerMap()
this.log = components.logger.forComponent('libp2p:connection-manager:dial-queue')
this.components = components
this.resolvers = init.resolvers ?? defaultOptions.resolvers
this.shutDownController = new AbortController()
setMaxListeners(Infinity, this.shutDownController.signal)
// controls dial concurrency
this.queue = new PriorityQueue({
concurrency: init.maxParallelDials ?? defaultOptions.maxParallelDials,
metricName: 'libp2p_dial_queue',
metrics: components.metrics
})
// a started job errored
this.queue.addEventListener('failure', (event) => {
if (event.detail?.error.name !== AbortError.name) {
this.log.error('error in dial queue - %e', event.detail)
}
})
}
start (): void {
this.shutDownController = new AbortController()
setMaxListeners(Infinity, this.shutDownController.signal)
}
/**
* Clears any pending dials
*/
stop (): void {
this.shutDownController.abort()
this.queue.abort()
}
/**
* Connects to a given peer, multiaddr or list of multiaddrs.
*
* If a peer is passed, all known multiaddrs will be tried. If a multiaddr or
* multiaddrs are passed only those will be dialled.
*
* Where a list of multiaddrs is passed, if any contain a peer id then all
* multiaddrs in the list must contain the same peer id.
*
* The dial to the first address that is successfully able to upgrade a
* connection will be used, all other dials will be aborted when that happens.
*/
async dial (peerIdOrMultiaddr: PeerId | Multiaddr | Multiaddr[], options: OpenConnectionOptions = {}): Promise<Connection> {
const { peerId, multiaddrs } = getPeerAddress(peerIdOrMultiaddr)
// make sure we don't have an existing non-limited connection to any of the
// addresses we are about to dial
const existingConnection = Array.from(this.connections.values()).flat().find(conn => {
if (options.force === true) {
return false
}
if (conn.limits != null) {
return false
}
if (conn.remotePeer.equals(peerId)) {
return true
}
return multiaddrs.find(addr => {
return addr.equals(conn.remoteAddr)
})
})
if (existingConnection?.status === 'open') {
this.log('already connected to %a', existingConnection.remoteAddr)
options.onProgress?.(new CustomProgressEvent('dial-queue:already-connected'))
return existingConnection
}
// ready to dial, all async work finished - make sure we don't have any
// pending dials in progress for this peer or set of multiaddrs
const existingDial = this.queue.queue.find(job => {
if (peerId?.equals(job.options.peerId) === true) {
return true
}
// does the dial contain any of the target multiaddrs?
const addresses = job.options.multiaddrs
if (addresses == null) {
return false
}
for (const multiaddr of multiaddrs) {
if (addresses.has(multiaddr.toString())) {
return true
}
}
return false
})
if (existingDial != null) {
this.log('joining existing dial target for %p', peerId)
// add all multiaddrs to the dial target
for (const multiaddr of multiaddrs) {
existingDial.options.multiaddrs.add(multiaddr.toString())
}
options.onProgress?.(new CustomProgressEvent('dial-queue:already-in-dial-queue'))
return existingDial.join(options)
}
if (this.queue.size >= this.maxDialQueueLength) {
throw new DialError('Dial queue is full')
}
this.log('creating dial target for %p', peerId, multiaddrs.map(ma => ma.toString()))
options.onProgress?.(new CustomProgressEvent('dial-queue:add-to-dial-queue'))
return this.queue.add(async (options) => {
options.onProgress?.(new CustomProgressEvent('dial-queue:start-dial'))
// create abort conditions - need to do this before `calculateMultiaddrs` as
// we may be about to resolve a dns addr which can time out
const signal = anySignal([
this.shutDownController.signal,
options.signal
])
setMaxListeners(Infinity, signal)
try {
return await this.dialPeer(options, signal)
} finally {
// clean up abort signals/controllers
signal.clear()
}
}, {
peerId,
priority: options.priority ?? DEFAULT_DIAL_PRIORITY,
multiaddrs: new Set(multiaddrs.map(ma => ma.toString())),
signal: options.signal ?? AbortSignal.timeout(this.dialTimeout),
onProgress: options.onProgress
})
}
private async dialPeer (options: DialQueueJobOptions, signal: AbortSignal): Promise<Connection> {
const peerId = options.peerId
const multiaddrs = options.multiaddrs
const failedMultiaddrs = new Set<string>()
// if we have no multiaddrs, only a peer id, set a flag so we will look the
// peer up in the peer routing to obtain multiaddrs
let forcePeerLookup = options.multiaddrs.size === 0
let dialed = 0
let dialIteration = 0
const errors: Error[] = []
this.log('starting dial to %p', peerId)
// repeat this operation in case addresses are added to the dial while we
// resolve multiaddrs, etc
while (forcePeerLookup || multiaddrs.size > 0) {
dialIteration++
// only perform peer lookup once
forcePeerLookup = false
// the addresses we will dial
const addrsToDial: Address[] = []
// copy the addresses into a new set
const addrs = new Set(options.multiaddrs)
// empty the old set - subsequent dial attempts for the same peer id may
// add more addresses to try
multiaddrs.clear()
this.log('calculating addrs to dial %p from %s', peerId, [...addrs])
// load addresses from address book, resolve and dnsaddrs, filter
// undialables, add peer IDs, etc
const calculatedAddrs = await this.calculateMultiaddrs(peerId, addrs, {
...options,
signal
})
for (const addr of calculatedAddrs) {
// skip any addresses we have previously failed to dial
if (failedMultiaddrs.has(addr.multiaddr.toString())) {
this.log.trace('skipping previously failed multiaddr %a while dialing %p', addr.multiaddr, peerId)
continue
}
addrsToDial.push(addr)
}
this.log('%s dial to %p with %s', dialIteration === 1 ? 'starting' : 'continuing', peerId, addrsToDial.map(ma => ma.multiaddr.toString()))
options?.onProgress?.(new CustomProgressEvent<Address[]>('dial-queue:calculated-addresses', addrsToDial))
for (const address of addrsToDial) {
if (dialed === this.maxPeerAddrsToDial) {
this.log('dialed maxPeerAddrsToDial (%d) addresses for %p, not trying any others', dialed, options.peerId)
throw new DialError('Peer had more than maxPeerAddrsToDial')
}
dialed++
try {
// try to dial the address
const conn = await this.components.transportManager.dial(address.multiaddr, {
...options,
signal
})
this.log('dial to %a succeeded', address.multiaddr)
// record the successful dial and the address
try {
await this.components.peerStore.merge(conn.remotePeer, {
multiaddrs: [
conn.remoteAddr
],
metadata: {
[LAST_DIAL_SUCCESS_KEY]: uint8ArrayFromString(Date.now().toString())
}
})
} catch (err: any) {
this.log.error('could not update last dial failure key for %p', peerId, err)
}
// dial successful, return the connection
return conn
} catch (err: any) {
this.log.error('dial failed to %a', address.multiaddr, err)
// ensure we don't dial it again in this attempt
failedMultiaddrs.add(address.multiaddr.toString())
if (peerId != null) {
// record the failed dial
try {
await this.components.peerStore.merge(peerId, {
metadata: {
[LAST_DIAL_FAILURE_KEY]: uint8ArrayFromString(Date.now().toString())
}
})
} catch (err: any) {
this.log.error('could not update last dial failure key for %p', peerId, err)
}
}
// the user/dial timeout/shutdown controller signal aborted
if (signal.aborted) {
throw new TimeoutError(err.message)
}
errors.push(err)
}
}
}
if (errors.length === 1) {
throw errors[0]
}
throw new AggregateError(errors, 'All multiaddr dials failed')
}
// eslint-disable-next-line complexity
private async calculateMultiaddrs (peerId?: PeerId, multiaddrs: Set<string> = new Set<string>(), options: OpenConnectionOptions = {}): Promise<Address[]> {
const addrs: Address[] = [...multiaddrs].map(ma => ({
multiaddr: multiaddr(ma),
isCertified: false
}))
// if a peer id or multiaddr(s) with a peer id, make sure it isn't our peer id and that we are allowed to dial it
if (peerId != null) {
if (this.components.peerId.equals(peerId)) {
throw new DialError('Tried to dial self')
}
if ((await this.components.connectionGater.denyDialPeer?.(peerId)) === true) {
throw new DialDeniedError('The dial request is blocked by gater.allowDialPeer')
}
// if just a peer id was passed, load available multiaddrs for this peer
// from the peer store
if (addrs.length === 0) {
this.log('loading multiaddrs for %p', peerId)
try {
const peer = await this.components.peerStore.get(peerId)
addrs.push(...peer.addresses)
this.log('loaded multiaddrs for %p', peerId, addrs.map(({ multiaddr }) => multiaddr.toString()))
} catch (err: any) {
if (err.name !== 'NotFoundError') {
throw err
}
}
}
// if we still don't have any addresses for this peer, or the only
// addresses we have are without any routing information (e.g.
// `/p2p/Qmfoo`), try a lookup using the peer routing
if (addrs.length === 0) {
this.log('looking up multiaddrs for %p in the peer routing', peerId)
try {
const peerInfo = await this.components.peerRouting.findPeer(peerId, options)
this.log('found multiaddrs for %p in the peer routing', peerId, addrs.map(({ multiaddr }) => multiaddr.toString()))
addrs.push(...peerInfo.multiaddrs.map(multiaddr => ({
multiaddr,
isCertified: false
})))
} catch (err: any) {
if (err.name === 'NoPeerRoutersError') {
this.log('no peer routers configured', peerId)
} else {
this.log.error('looking up multiaddrs for %p in the peer routing failed - %e', peerId, err)
}
}
}
}
// resolve addresses - this can result in a one-to-many translation when
// dnsaddrs are resolved
let resolvedAddresses = (await Promise.all(
addrs.map(async addr => {
const result = await resolveMultiaddr(addr.multiaddr, this.resolvers, {
dns: this.components.dns,
log: this.log,
...options
})
if (result.length === 1 && result[0].equals(addr.multiaddr)) {
return addr
}
return result.map(multiaddr => ({
multiaddr,
isCertified: false
}))
})
))
.flat()
// ensure the peer id is appended to the multiaddr
if (peerId != null) {
const peerIdMultiaddr = `/p2p/${peerId.toString()}`
resolvedAddresses = resolvedAddresses.map(addr => {
const lastComponent = addr.multiaddr.getComponents().pop()
// append peer id to multiaddr if it is not already present
if (lastComponent?.name !== 'p2p') {
return {
multiaddr: addr.multiaddr.encapsulate(peerIdMultiaddr),
isCertified: addr.isCertified
}
}
return addr
})
}
const filteredAddrs = resolvedAddresses.filter(addr => {
// filter out any multiaddrs that we do not have transports for
if (this.components.transportManager.dialTransportForMultiaddr(addr.multiaddr) == null) {
return false
}
// if the resolved multiaddr has a PeerID but it's the wrong one, ignore it
// - this can happen with addresses like bootstrap.libp2p.io that resolve
// to multiple different peers
const addrPeerId = addr.multiaddr.getPeerId()
if (peerId != null && addrPeerId != null) {
return peerId.equals(addrPeerId)
}
return true
})
// deduplicate addresses
const dedupedAddrs = new Map<string, Address>()
for (const addr of filteredAddrs) {
const maStr = addr.multiaddr.toString()
const existing = dedupedAddrs.get(maStr)
if (existing != null) {
existing.isCertified = existing.isCertified || addr.isCertified || false
continue
}
dedupedAddrs.set(maStr, addr)
}
const dedupedMultiaddrs = [...dedupedAddrs.values()]
// make sure we actually have some addresses to dial
if (dedupedMultiaddrs.length === 0) {
throw new NoValidAddressesError('The dial request has no valid addresses')
}
const gatedAddrs: Address[] = []
for (const addr of dedupedMultiaddrs) {
if (this.components.connectionGater.denyDialMultiaddr != null && await this.components.connectionGater.denyDialMultiaddr(addr.multiaddr)) {
continue
}
gatedAddrs.push(addr)
}
const sortedGatedAddrs = this.addressSorter == null ? defaultAddressSorter(gatedAddrs) : gatedAddrs.sort(this.addressSorter)
// make sure we actually have some addresses to dial
if (sortedGatedAddrs.length === 0) {
throw new DialDeniedError('The connection gater denied all addresses in the dial request')
}
this.log.trace('addresses for %p before filtering', peerId ?? 'unknown peer', resolvedAddresses.map(({ multiaddr }) => multiaddr.toString()))
this.log.trace('addresses for %p after filtering', peerId ?? 'unknown peer', sortedGatedAddrs.map(({ multiaddr }) => multiaddr.toString()))
return sortedGatedAddrs
}
async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise<boolean> {
if (!Array.isArray(multiaddr)) {
multiaddr = [multiaddr]
}
try {
const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options)
if (options.runOnLimitedConnection === false) {
// return true if any resolved multiaddrs are not relay addresses
return addresses.find(addr => {
return !Circuit.matches(addr.multiaddr)
}) != null
}
return true
} catch (err) {
this.log.trace('error calculating if multiaddr(s) were dialable', err)
}
return false
}
}