libp2p
Version:
JavaScript implementation of libp2p, a modular peer to peer network stack
524 lines (439 loc) • 18 kB
text/typescript
import { InvalidMultiaddrError, InvalidPeerIdError } from '@libp2p/interface'
import * as mss from '@libp2p/multistream-select'
import { peerIdFromString } from '@libp2p/peer-id'
import { trackedMap } from '@libp2p/utils/tracked-map'
import { anySignal } from 'any-signal'
import { setMaxListeners } from 'main-event'
import { CustomProgressEvent } from 'progress-events'
import { raceSignal } from 'race-signal'
import { PROTOCOL_NEGOTIATION_TIMEOUT, INBOUND_UPGRADE_TIMEOUT } from './connection-manager/constants.js'
import { createConnection } from './connection.js'
import { ConnectionDeniedError, ConnectionInterceptedError, EncryptionFailedError, MuxerUnavailableError } from './errors.js'
import type { Libp2pEvents, AbortOptions, ComponentLogger, MultiaddrConnection, Connection, ConnectionProtector, ConnectionEncrypter, SecuredConnection, ConnectionGater, Metrics, PeerId, PeerStore, StreamMuxerFactory, Upgrader as UpgraderInterface, UpgraderOptions, ConnectionLimits, SecureConnectionOptions, CounterGroup, ClearableSignal } from '@libp2p/interface'
import type { ConnectionManager, Registrar } from '@libp2p/interface-internal'
import type { TypedEventTarget } from 'main-event'
interface CreateConnectionOptions {
id: string
cryptoProtocol: string
direction: 'inbound' | 'outbound'
maConn: MultiaddrConnection
upgradedConn: MultiaddrConnection
remotePeer: PeerId
muxerFactory?: StreamMuxerFactory
limits?: ConnectionLimits
}
export interface CryptoResult extends SecuredConnection<MultiaddrConnection> {
protocol: string
}
export interface UpgraderInit {
connectionEncrypters: ConnectionEncrypter[]
streamMuxers: StreamMuxerFactory[]
/**
* An amount of ms by which an inbound connection upgrade must complete
*
* @default 3000
*/
inboundUpgradeTimeout?: number
/**
* When a new incoming stream is opened on a multiplexed connection, protocol
* negotiation on that stream must complete within this many ms
*
* @default 2000
*/
inboundStreamProtocolNegotiationTimeout?: number
/**
* When a new incoming stream is opened on a multiplexed connection, protocol
* negotiation on that stream must complete within this many ms
*
* @default 2000
*/
outboundStreamProtocolNegotiationTimeout?: number
}
export interface UpgraderComponents {
peerId: PeerId
metrics?: Metrics
connectionManager: ConnectionManager
connectionGater: ConnectionGater
connectionProtector?: ConnectionProtector
registrar: Registrar
peerStore: PeerStore
events: TypedEventTarget<Libp2pEvents>
logger: ComponentLogger
}
type ConnectionDeniedType = keyof Pick<ConnectionGater, 'denyOutboundConnection' | 'denyInboundEncryptedConnection' | 'denyOutboundEncryptedConnection' | 'denyInboundUpgradedConnection' | 'denyOutboundUpgradedConnection'>
export class Upgrader implements UpgraderInterface {
private readonly components: UpgraderComponents
private readonly connectionEncrypters: Map<string, ConnectionEncrypter>
private readonly streamMuxers: Map<string, StreamMuxerFactory>
private readonly inboundUpgradeTimeout: number
private readonly inboundStreamProtocolNegotiationTimeout: number
private readonly outboundStreamProtocolNegotiationTimeout: number
private readonly events: TypedEventTarget<Libp2pEvents>
private readonly metrics: {
dials?: CounterGroup<'inbound' | 'outbound'>
errors?: CounterGroup<'inbound' | 'outbound'>
inboundErrors?: CounterGroup
outboundErrors?: CounterGroup
}
constructor (components: UpgraderComponents, init: UpgraderInit) {
this.components = components
this.connectionEncrypters = trackedMap({
name: 'libp2p_upgrader_connection_encrypters',
metrics: this.components.metrics
})
init.connectionEncrypters.forEach(encrypter => {
this.connectionEncrypters.set(encrypter.protocol, encrypter)
})
this.streamMuxers = trackedMap({
name: 'libp2p_upgrader_stream_multiplexers',
metrics: this.components.metrics
})
init.streamMuxers.forEach(muxer => {
this.streamMuxers.set(muxer.protocol, muxer)
})
this.inboundUpgradeTimeout = init.inboundUpgradeTimeout ?? INBOUND_UPGRADE_TIMEOUT
this.inboundStreamProtocolNegotiationTimeout = init.inboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT
this.outboundStreamProtocolNegotiationTimeout = init.outboundStreamProtocolNegotiationTimeout ?? PROTOCOL_NEGOTIATION_TIMEOUT
this.events = components.events
this.metrics = {
dials: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_total'),
errors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dial_errors_total'),
inboundErrors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_inbound_errors_total'),
outboundErrors: components.metrics?.registerCounterGroup('libp2p_connection_manager_dials_outbound_errors_total')
}
}
readonly [Symbol.toStringTag] = '@libp2p/upgrader'
async shouldBlockConnection (connectionType: 'denyInboundConnection', maConn: MultiaddrConnection): Promise<void>
async shouldBlockConnection (connectionType: ConnectionDeniedType, remotePeer: PeerId, maConn: MultiaddrConnection): Promise<void>
async shouldBlockConnection (method: ConnectionDeniedType | 'denyInboundConnection', ...args: any[]): Promise<void> {
const denyOperation: any = this.components.connectionGater[method]
if (denyOperation == null) {
return
}
const result = await denyOperation.apply(this.components.connectionGater, args)
if (result === true) {
throw new ConnectionInterceptedError(`The multiaddr connection is blocked by gater.${method}`)
}
}
createInboundAbortSignal (signal: AbortSignal): ClearableSignal {
const output = anySignal([
AbortSignal.timeout(this.inboundUpgradeTimeout),
signal
])
setMaxListeners(Infinity, output)
return output
}
/**
* Upgrades an inbound connection
*/
async upgradeInbound (maConn: MultiaddrConnection, opts: UpgraderOptions): Promise<void> {
let accepted = false
// always apply upgrade timeout for incoming upgrades
const signal = this.createInboundAbortSignal(opts.signal)
try {
this.metrics.dials?.increment({
inbound: true
})
accepted = await raceSignal(this.components.connectionManager.acceptIncomingConnection(maConn), signal)
if (!accepted) {
throw new ConnectionDeniedError('Connection denied')
}
await raceSignal(this.shouldBlockConnection('denyInboundConnection', maConn), signal)
await this._performUpgrade(maConn, 'inbound', {
...opts,
signal
})
} catch (err: any) {
this.metrics.errors?.increment({
inbound: true
})
this.metrics.inboundErrors?.increment({
[err.name ?? 'Error']: true
})
throw err
} finally {
signal.clear()
if (accepted) {
this.components.connectionManager.afterUpgradeInbound()
}
}
}
/**
* Upgrades an outbound connection
*/
async upgradeOutbound (maConn: MultiaddrConnection, opts: UpgraderOptions): Promise<Connection> {
try {
this.metrics.dials?.increment({
outbound: true
})
const idStr = maConn.remoteAddr.getPeerId()
let remotePeerId: PeerId | undefined
if (idStr != null) {
remotePeerId = peerIdFromString(idStr)
await raceSignal(this.shouldBlockConnection('denyOutboundConnection', remotePeerId, maConn), opts.signal)
}
let direction: 'inbound' | 'outbound' = 'outbound'
// act as the multistream-select server if we are not to be the initiator
if (opts.initiator === false) {
direction = 'inbound'
}
return await this._performUpgrade(maConn, direction, opts)
} catch (err: any) {
this.metrics.errors?.increment({
outbound: true
})
this.metrics.outboundErrors?.increment({
[err.name ?? 'Error']: true
})
throw err
}
}
private async _performUpgrade (maConn: MultiaddrConnection, direction: 'inbound' | 'outbound', opts: UpgraderOptions): Promise<Connection> {
let encryptedConn: MultiaddrConnection
let remotePeer: PeerId
let upgradedConn: MultiaddrConnection
let muxerFactory: StreamMuxerFactory | undefined
let cryptoProtocol
const id = `${(parseInt(String(Math.random() * 1e9))).toString(36)}${Date.now()}`
maConn.log = maConn.log.newScope(`${direction}:${id}`)
this.components.metrics?.trackMultiaddrConnection(maConn)
maConn.log.trace('starting the %s connection upgrade', direction)
// Protect
let protectedConn = maConn
if (opts?.skipProtection !== true) {
const protector = this.components.connectionProtector
if (protector != null) {
maConn.log('protecting the %s connection', direction)
protectedConn = await protector.protect(maConn, opts)
}
}
try {
// Encrypt the connection
encryptedConn = protectedConn
if (opts?.skipEncryption !== true) {
opts?.onProgress?.(new CustomProgressEvent(`upgrader:encrypt-${direction}-connection`));
({
conn: encryptedConn,
remotePeer,
protocol: cryptoProtocol,
streamMuxer: muxerFactory
} = await (direction === 'inbound'
? this._encryptInbound(protectedConn, opts)
: this._encryptOutbound(protectedConn, opts)
))
const maConn: MultiaddrConnection = {
...protectedConn,
...encryptedConn
}
await this.shouldBlockConnection(direction === 'inbound' ? 'denyInboundEncryptedConnection' : 'denyOutboundEncryptedConnection', remotePeer, maConn)
} else {
const idStr = maConn.remoteAddr.getPeerId()
if (idStr == null) {
throw new InvalidMultiaddrError(`${direction} connection that skipped encryption must have a peer id`)
}
const remotePeerId = peerIdFromString(idStr)
cryptoProtocol = 'native'
remotePeer = remotePeerId
}
// this can happen if we dial a multiaddr without a peer id, we only find
// out the identity of the remote after the connection is encrypted
if (remotePeer.equals(this.components.peerId)) {
const err = new InvalidPeerIdError('Can not dial self')
maConn.abort(err)
throw err
}
upgradedConn = encryptedConn
if (opts?.muxerFactory != null) {
muxerFactory = opts.muxerFactory
} else if (muxerFactory == null && this.streamMuxers.size > 0) {
opts?.onProgress?.(new CustomProgressEvent(`upgrader:multiplex-${direction}-connection`))
// Multiplex the connection
const multiplexed = await (direction === 'inbound'
? this._multiplexInbound({
...protectedConn,
...encryptedConn
}, this.streamMuxers, opts)
: this._multiplexOutbound({
...protectedConn,
...encryptedConn
}, this.streamMuxers, opts))
muxerFactory = multiplexed.muxerFactory
upgradedConn = multiplexed.stream
}
} catch (err: any) {
maConn.log.error('failed to upgrade inbound connection %s %a - %e', direction === 'inbound' ? 'from' : 'to', maConn.remoteAddr, err)
throw err
}
await this.shouldBlockConnection(direction === 'inbound' ? 'denyInboundUpgradedConnection' : 'denyOutboundUpgradedConnection', remotePeer, maConn)
const conn = this._createConnection({
id,
cryptoProtocol,
direction,
maConn,
upgradedConn,
muxerFactory,
remotePeer,
limits: opts?.limits
})
conn.log('successfully upgraded %s connection', direction)
return conn
}
/**
* A convenience method for generating a new `Connection`
*/
_createConnection (opts: CreateConnectionOptions): Connection {
const {
id,
cryptoProtocol,
direction,
maConn,
upgradedConn,
remotePeer,
muxerFactory,
limits
} = opts
let connection: Connection // eslint-disable-line prefer-const
const _timeline = maConn.timeline
maConn.timeline = new Proxy(_timeline, {
set: (...args) => {
if (args[1] === 'close' && args[2] != null && _timeline.close == null) {
// Wait for close to finish before notifying of the closure
(async () => {
try {
if (connection.status === 'open') {
await connection.close()
}
} catch (err: any) {
connection.log.error('error closing connection after timeline close %e', err)
} finally {
this.events.safeDispatchEvent('connection:close', {
detail: connection
})
}
})().catch(err => {
connection.log.error('error thrown while dispatching connection:close event %e', err)
})
}
return Reflect.set(...args)
}
})
maConn.timeline.upgraded = Date.now()
// Create the connection
connection = createConnection(this.components, {
id,
maConn: upgradedConn,
remotePeer,
direction,
muxerFactory,
encryption: cryptoProtocol,
limits,
outboundStreamProtocolNegotiationTimeout: this.outboundStreamProtocolNegotiationTimeout,
inboundStreamProtocolNegotiationTimeout: this.inboundStreamProtocolNegotiationTimeout
})
this.events.safeDispatchEvent('connection:open', {
detail: connection
})
return connection
}
/**
* Attempts to encrypt the incoming `connection` with the provided `cryptos`
*/
async _encryptInbound (connection: MultiaddrConnection, options?: AbortOptions): Promise<CryptoResult> {
const protocols = Array.from(this.connectionEncrypters.keys())
try {
const { stream, protocol } = await mss.handle(connection, protocols, {
...options,
log: connection.log
})
const encrypter = this.connectionEncrypters.get(protocol)
if (encrypter == null) {
throw new EncryptionFailedError(`no crypto module found for ${protocol}`)
}
connection.log('encrypting inbound connection to %a using %s', connection.remoteAddr, protocol)
return {
...await encrypter.secureInbound(stream, options),
protocol
}
} catch (err: any) {
connection.log.error('encrypting inbound connection from %a failed', connection.remoteAddr, err)
throw new EncryptionFailedError(err.message)
}
}
/**
* Attempts to encrypt the given `connection` with the provided connection encrypters.
* The first `ConnectionEncrypter` module to succeed will be used
*/
async _encryptOutbound (connection: MultiaddrConnection, options: SecureConnectionOptions): Promise<CryptoResult> {
const protocols = Array.from(this.connectionEncrypters.keys())
try {
connection.log.trace('selecting encrypter from %s', protocols)
const { stream, protocol } = await mss.select(connection, protocols, {
...options,
log: connection.log,
yieldBytes: true
})
const encrypter = this.connectionEncrypters.get(protocol)
if (encrypter == null) {
throw new EncryptionFailedError(`no crypto module found for ${protocol}`)
}
connection.log('encrypting outbound connection to %a using %s', connection.remoteAddr, protocol)
return {
...await encrypter.secureOutbound(stream, options),
protocol
}
} catch (err: any) {
connection.log.error('encrypting outbound connection to %a failed', connection.remoteAddr, err)
throw new EncryptionFailedError(err.message)
}
}
/**
* Selects one of the given muxers via multistream-select. That
* muxer will be used for all future streams on the connection.
*/
async _multiplexOutbound (connection: MultiaddrConnection, muxers: Map<string, StreamMuxerFactory>, options: AbortOptions): Promise<{ stream: MultiaddrConnection, muxerFactory?: StreamMuxerFactory }> {
const protocols = Array.from(muxers.keys())
connection.log('outbound selecting muxer %s', protocols)
try {
connection.log.trace('selecting stream muxer from %s', protocols)
const {
stream,
protocol
} = await mss.select(connection, protocols, {
...options,
log: connection.log,
yieldBytes: true
})
connection.log('selected %s as muxer protocol', protocol)
const muxerFactory = muxers.get(protocol)
return { stream, muxerFactory }
} catch (err: any) {
connection.log.error('error multiplexing outbound connection', err)
throw new MuxerUnavailableError(String(err))
}
}
/**
* Registers support for one of the given muxers via multistream-select. The
* selected muxer will be used for all future streams on the connection.
*/
async _multiplexInbound (connection: MultiaddrConnection, muxers: Map<string, StreamMuxerFactory>, options: AbortOptions): Promise<{ stream: MultiaddrConnection, muxerFactory?: StreamMuxerFactory }> {
const protocols = Array.from(muxers.keys())
connection.log('inbound handling muxers %s', protocols)
try {
const { stream, protocol } = await mss.handle(connection, protocols, {
...options,
log: connection.log
})
const muxerFactory = muxers.get(protocol)
return { stream, muxerFactory }
} catch (err: any) {
connection.log.error('error multiplexing inbound connection', err)
throw new MuxerUnavailableError(String(err))
}
}
getConnectionEncrypters (): Map<string, ConnectionEncrypter<unknown>> {
return this.connectionEncrypters
}
getStreamMuxers (): Map<string, StreamMuxerFactory> {
return this.streamMuxers
}
}