UNPKG

libp2p

Version:

JavaScript implementation of libp2p, a modular peer to peer network stack

411 lines • 17 kB
import { ConnectionClosedError, InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'; import { PeerMap } from '@libp2p/peer-collections'; import { RateLimiter } from '@libp2p/utils/rate-limiter'; import { multiaddr } from '@multiformats/multiaddr'; import { CustomProgressEvent } from 'progress-events'; import { getPeerAddress } from '../get-peer.js'; import { ConnectionPruner } from './connection-pruner.js'; import { DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_DIAL_QUEUE_LENGTH, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL } from './constants.js'; import { DialQueue } from './dial-queue.js'; import { ReconnectQueue } from './reconnect-queue.js'; import { dnsaddrResolver } from "./resolvers/index.js"; import { multiaddrToIpNet } from './utils.js'; export const DEFAULT_DIAL_PRIORITY = 50; const defaultOptions = { maxConnections: MAX_CONNECTIONS, inboundConnectionThreshold: INBOUND_CONNECTION_THRESHOLD, maxIncomingPendingConnections: MAX_INCOMING_PENDING_CONNECTIONS }; /** * Responsible for managing known connections. */ export class DefaultConnectionManager { started; connections; allow; deny; maxIncomingPendingConnections; incomingPendingConnections; outboundPendingConnections; maxConnections; dialQueue; reconnectQueue; connectionPruner; inboundConnectionRateLimiter; peerStore; metrics; events; log; peerId; constructor(components, init = {}) { this.maxConnections = init.maxConnections ?? defaultOptions.maxConnections; if (this.maxConnections < 1) { throw new InvalidParametersError('Connection Manager maxConnections must be greater than 0'); } /** * Map of connections per peer */ this.connections = new PeerMap(); this.started = false; this.peerId = components.peerId; this.peerStore = components.peerStore; this.metrics = components.metrics; this.events = components.events; this.log = components.logger.forComponent('libp2p:connection-manager'); this.onConnect = this.onConnect.bind(this); this.onDisconnect = this.onDisconnect.bind(this); // allow/deny lists this.allow = (init.allow ?? []).map(str => multiaddrToIpNet(str)); this.deny = (init.deny ?? []).map(str => multiaddrToIpNet(str)); this.incomingPendingConnections = 0; this.maxIncomingPendingConnections = init.maxIncomingPendingConnections ?? defaultOptions.maxIncomingPendingConnections; this.outboundPendingConnections = 0; // controls individual peers trying to dial us too quickly this.inboundConnectionRateLimiter = new RateLimiter({ points: init.inboundConnectionThreshold ?? defaultOptions.inboundConnectionThreshold, duration: 1 }); // controls what happens when we have too many connections this.connectionPruner = new ConnectionPruner({ connectionManager: this, peerStore: components.peerStore, events: components.events, logger: components.logger }, { allow: init.allow?.map(a => multiaddr(a)) }); this.dialQueue = new DialQueue(components, { addressSorter: init.addressSorter, maxParallelDials: init.maxParallelDials ?? MAX_PARALLEL_DIALS, maxDialQueueLength: init.maxDialQueueLength ?? MAX_DIAL_QUEUE_LENGTH, maxPeerAddrsToDial: init.maxPeerAddrsToDial ?? MAX_PEER_ADDRS_TO_DIAL, dialTimeout: init.dialTimeout ?? DIAL_TIMEOUT, resolvers: init.resolvers ?? { dnsaddr: dnsaddrResolver }, connections: this.connections }); this.reconnectQueue = new ReconnectQueue({ events: components.events, peerStore: components.peerStore, logger: components.logger, connectionManager: this }, { retries: init.reconnectRetries, retryInterval: init.reconnectRetryInterval, backoffFactor: init.reconnectBackoffFactor, maxParallelReconnects: init.maxParallelReconnects }); } [Symbol.toStringTag] = '@libp2p/connection-manager'; /** * Starts the Connection Manager. If Metrics are not enabled on libp2p * only event loop and connection limits will be monitored. */ async start() { // track inbound/outbound connections this.metrics?.registerMetricGroup('libp2p_connection_manager_connections', { calculate: () => { const metric = { inbound: 0, 'inbound pending': this.incomingPendingConnections, outbound: 0, 'outbound pending': this.outboundPendingConnections }; for (const conns of this.connections.values()) { for (const conn of conns) { metric[conn.direction]++; } } return metric; } }); // track total number of streams per protocol this.metrics?.registerMetricGroup('libp2p_protocol_streams_total', { label: 'protocol', calculate: () => { const metric = {}; for (const conns of this.connections.values()) { for (const conn of conns) { for (const stream of conn.streams) { const key = `${stream.direction} ${stream.protocol ?? 'unnegotiated'}`; metric[key] = (metric[key] ?? 0) + 1; } } } return metric; } }); // track 90th percentile of streams per protocol this.metrics?.registerMetricGroup('libp2p_connection_manager_protocol_streams_per_connection_90th_percentile', { label: 'protocol', calculate: () => { const allStreams = {}; for (const conns of this.connections.values()) { for (const conn of conns) { const streams = {}; for (const stream of conn.streams) { const key = `${stream.direction} ${stream.protocol ?? 'unnegotiated'}`; streams[key] = (streams[key] ?? 0) + 1; } for (const [protocol, count] of Object.entries(streams)) { allStreams[protocol] = allStreams[protocol] ?? []; allStreams[protocol].push(count); } } } const metric = {}; for (let [protocol, counts] of Object.entries(allStreams)) { counts = counts.sort((a, b) => a - b); const index = Math.floor(counts.length * 0.9); metric[protocol] = counts[index]; } return metric; } }); this.events.addEventListener('connection:open', this.onConnect); this.events.addEventListener('connection:close', this.onDisconnect); await start(this.dialQueue, this.reconnectQueue, this.connectionPruner); this.started = true; this.log('started'); } /** * Stops the Connection Manager */ async stop() { this.events.removeEventListener('connection:open', this.onConnect); this.events.removeEventListener('connection:close', this.onDisconnect); await stop(this.reconnectQueue, this.dialQueue, this.connectionPruner); // Close all connections we're tracking const tasks = []; for (const connectionList of this.connections.values()) { for (const connection of connectionList) { tasks.push((async () => { try { await connection.close(); } catch (err) { this.log.error(err); } })()); } } this.log('closing %d connections', tasks.length); await Promise.all(tasks); this.connections.clear(); this.log('stopped'); } getMaxConnections() { return this.maxConnections; } setMaxConnections(maxConnections) { if (this.maxConnections < 1) { throw new InvalidParametersError('Connection Manager maxConnections must be greater than 0'); } let needsPrune = false; if (maxConnections < this.maxConnections) { needsPrune = true; } this.maxConnections = maxConnections; if (needsPrune) { this.connectionPruner.maybePruneConnections(); } } onConnect(evt) { void this._onConnect(evt).catch(err => { this.log.error(err); }); } /** * Tracks the incoming connection and check the connection limit */ async _onConnect(evt) { const { detail: connection } = evt; if (!this.started) { // This can happen when we are in the process of shutting down the node await connection.close(); return; } if (connection.status !== 'open') { // this can happen when the remote closes the connection immediately after // opening return; } const peerId = connection.remotePeer; const isNewPeer = !this.connections.has(peerId); const storedConns = this.connections.get(peerId) ?? []; storedConns.push(connection); this.connections.set(peerId, storedConns); // only need to store RSA public keys, all other types are embedded in the peer id if (peerId.publicKey != null && peerId.type === 'RSA') { await this.peerStore.patch(peerId, { publicKey: peerId.publicKey }); } if (isNewPeer) { this.events.safeDispatchEvent('peer:connect', { detail: connection.remotePeer }); } } /** * Removes the connection from tracking */ onDisconnect(evt) { const { detail: connection } = evt; const peerId = connection.remotePeer; const peerConns = this.connections.get(peerId) ?? []; // remove closed connection const filteredPeerConns = peerConns.filter(conn => conn.id !== connection.id); // update peer connections this.connections.set(peerId, filteredPeerConns); if (filteredPeerConns.length === 0) { // trigger disconnect event if no connections remain this.log.trace('peer %p disconnected, removing connection map entry', peerId); this.connections.delete(peerId); // broadcast disconnect event this.events.safeDispatchEvent('peer:disconnect', { detail: connection.remotePeer }); } } getConnections(peerId) { if (peerId != null) { return this.connections.get(peerId) ?? []; } let conns = []; for (const c of this.connections.values()) { conns = conns.concat(c); } return conns; } getConnectionsMap() { return this.connections; } async openConnection(peerIdOrMultiaddr, options = {}) { if (!this.started) { throw new NotStartedError('Not started'); } this.outboundPendingConnections++; try { options.signal?.throwIfAborted(); const { peerId } = getPeerAddress(peerIdOrMultiaddr); if (this.peerId.equals(peerId)) { throw new InvalidPeerIdError('Can not dial self'); } if (peerId != null && options.force !== true) { this.log('dial %p', peerId); const existingConnection = this.getConnections(peerId) .find(conn => conn.limits == null); if (existingConnection != null) { this.log('had an existing non-limited connection to %p as %a', peerId, existingConnection.remoteAddr); options.onProgress?.(new CustomProgressEvent('dial-queue:already-connected')); return existingConnection; } } const connection = await this.dialQueue.dial(peerIdOrMultiaddr, { ...options, priority: options.priority ?? DEFAULT_DIAL_PRIORITY }); if (connection.status !== 'open') { throw new ConnectionClosedError('Remote closed connection during opening'); } let peerConnections = this.connections.get(connection.remotePeer); if (peerConnections == null) { peerConnections = []; this.connections.set(connection.remotePeer, peerConnections); } // we get notified of connections via the Upgrader emitting "connection" // events, double check we aren't already tracking this connection before // storing it let trackedConnection = false; for (const conn of peerConnections) { if (conn.id === connection.id) { trackedConnection = true; } // make sure we don't already have a connection to this multiaddr if (options.force !== true && conn.id !== connection.id && conn.remoteAddr.equals(connection.remoteAddr)) { connection.abort(new InvalidMultiaddrError('Duplicate multiaddr connection')); // return the existing connection return conn; } } if (!trackedConnection) { peerConnections.push(connection); } return connection; } finally { this.outboundPendingConnections--; } } async closeConnections(peerId, options = {}) { const connections = this.connections.get(peerId) ?? []; await Promise.all(connections.map(async (connection) => { try { await connection.close(options); } catch (err) { connection.abort(err); } })); } async acceptIncomingConnection(maConn) { // check deny list const denyConnection = this.deny.some(ma => { return ma.contains(maConn.remoteAddr.nodeAddress().address); }); if (denyConnection) { this.log('connection from %a refused - connection remote address was in deny list', maConn.remoteAddr); return false; } // check allow list const allowConnection = this.allow.some(ipNet => { return ipNet.contains(maConn.remoteAddr.nodeAddress().address); }); if (allowConnection) { this.incomingPendingConnections++; return true; } // check pending connections if (this.incomingPendingConnections === this.maxIncomingPendingConnections) { this.log('connection from %a refused - incomingPendingConnections exceeded by host', maConn.remoteAddr); return false; } if (maConn.remoteAddr.isThinWaistAddress()) { const host = maConn.remoteAddr.nodeAddress().address; try { await this.inboundConnectionRateLimiter.consume(host, 1); } catch { this.log('connection from %a refused - inboundConnectionThreshold exceeded by host %s', maConn.remoteAddr, host); return false; } } if (this.getConnections().length < this.maxConnections) { this.incomingPendingConnections++; return true; } this.log('connection from %a refused - maxConnections exceeded', maConn.remoteAddr); return false; } afterUpgradeInbound() { this.incomingPendingConnections--; } getDialQueue() { const statusMap = { queued: 'queued', running: 'active', errored: 'error', complete: 'success' }; return this.dialQueue.queue.queue.map(job => { return { id: job.id, status: statusMap[job.status], peerId: job.options.peerId, multiaddrs: [...job.options.multiaddrs].map(ma => multiaddr(ma)) }; }); } async isDialable(multiaddr, options = {}) { return this.dialQueue.isDialable(multiaddr, options); } } //# sourceMappingURL=index.js.map