UNPKG

libp2p

Version:

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

544 lines (445 loc) • 17.6 kB
/* eslint-disable complexity */ import { isIPv4 } from '@chainsafe/is-ip' import { peerIdFromString } from '@libp2p/peer-id' import { debounce } from '@libp2p/utils/debounce' import { createScalableCuckooFilter } from '@libp2p/utils/filters' import { isPrivateIp } from '@libp2p/utils/private-ip' import { multiaddr } from '@multiformats/multiaddr' import { QUIC_V1, TCP, WebSockets, WebSocketsSecure } from '@multiformats/multiaddr-matcher' import { DNSMappings } from './dns-mappings.js' import { IPMappings } from './ip-mappings.js' import { ObservedAddresses } from './observed-addresses.js' import { TransportAddresses } from './transport-addresses.js' import type { ComponentLogger, Libp2pEvents, Logger, PeerId, PeerStore, Metrics } from '@libp2p/interface' import type { AddressManager as AddressManagerInterface, TransportManager, NodeAddress, ConfirmAddressOptions } from '@libp2p/interface-internal' import type { Filter } from '@libp2p/utils/filters' import type { Multiaddr } from '@multiformats/multiaddr' import type { TypedEventTarget } from 'main-event' const ONE_MINUTE = 60_000 export const defaultValues = { maxObservedAddresses: 10, addressVerificationTTL: ONE_MINUTE * 10, addressVerificationRetry: ONE_MINUTE * 5 } export interface AddressManagerInit { /** * Pass an function in this field to override the list of addresses * that are announced to the network */ announceFilter?: AddressFilter /** * A list of string multiaddrs to listen on */ listen?: string[] /** * A list of string multiaddrs to use instead of those reported by transports */ announce?: string[] /** * A list of string multiaddrs string to never announce */ noAnnounce?: string[] /** * A list of string multiaddrs to add to the list of announced addresses */ appendAnnounce?: string[] /** * Limits the number of observed addresses we will store */ maxObservedAddresses?: number /** * How long before each public address should be reverified in ms. * * Requires `@libp2p/autonat` or some other verification method to be * configured. * * @default 600_000 */ addressVerificationTTL?: number /** * After a transport or mapped address has failed to verify, how long to wait * before retrying it in ms * * Requires `@libp2p/autonat` or some other verification method to be * configured. * * @default 300_000 */ addressVerificationRetry?: number } export interface AddressManagerComponents { peerId: PeerId transportManager: TransportManager peerStore: PeerStore events: TypedEventTarget<Libp2pEvents> logger: ComponentLogger metrics?: Metrics } /** * A function that takes a list of multiaddrs and returns a list * to announce */ export interface AddressFilter { (addrs: Multiaddr[]): Multiaddr[] } const defaultAddressFilter = (addrs: Multiaddr[]): Multiaddr[] => addrs /** * If the passed multiaddr contains the passed peer id, remove it */ function stripPeerId (ma: Multiaddr, peerId: PeerId): Multiaddr { const observedPeerIdStr = ma.getPeerId() // strip our peer id if it has been passed if (observedPeerIdStr != null) { const observedPeerId = peerIdFromString(observedPeerIdStr) // use same encoding for comparison if (observedPeerId.equals(peerId)) { ma = ma.decapsulate(multiaddr(`/p2p/${peerId.toString()}`)) } } return ma } export class AddressManager implements AddressManagerInterface { private readonly log: Logger private readonly components: AddressManagerComponents // this is an array to allow for duplicates, e.g. multiples of `/ip4/0.0.0.0/tcp/0` private readonly listen: string[] private readonly announce: Set<string> private readonly appendAnnounce: Set<string> private readonly announceFilter: AddressFilter private readonly observed: ObservedAddresses private readonly dnsMappings: DNSMappings private readonly ipMappings: IPMappings private readonly transportAddresses: TransportAddresses private readonly observedAddressFilter: Filter private readonly addressVerificationTTL: number private readonly addressVerificationRetry: number /** * Responsible for managing the peer addresses. * Peers can specify their listen and announce addresses. * The listen addresses will be used by the libp2p transports to listen for new connections, * while the announce addresses will be used for the peer addresses' to other peers in the network. */ constructor (components: AddressManagerComponents, init: AddressManagerInit = {}) { const { listen = [], announce = [], appendAnnounce = [] } = init this.components = components this.log = components.logger.forComponent('libp2p:address-manager') this.listen = listen.map(ma => ma.toString()) this.announce = new Set(announce.map(ma => ma.toString())) this.appendAnnounce = new Set(appendAnnounce.map(ma => ma.toString())) this.observed = new ObservedAddresses(components, init) this.dnsMappings = new DNSMappings(components, init) this.ipMappings = new IPMappings(components, init) this.transportAddresses = new TransportAddresses(components, init) this.announceFilter = init.announceFilter ?? defaultAddressFilter this.observedAddressFilter = createScalableCuckooFilter(1024) this.addressVerificationTTL = init.addressVerificationTTL ?? defaultValues.addressVerificationTTL this.addressVerificationRetry = init.addressVerificationRetry ?? defaultValues.addressVerificationRetry // this method gets called repeatedly on startup when transports start listening so // debounce it so we don't cause multiple self:peer:update events to be emitted this._updatePeerStoreAddresses = debounce(this._updatePeerStoreAddresses.bind(this), 1000) // update our stored addresses when new transports listen components.events.addEventListener('transport:listening', () => { this._updatePeerStoreAddresses() }) // update our stored addresses when existing transports stop listening components.events.addEventListener('transport:close', () => { this._updatePeerStoreAddresses() }) } readonly [Symbol.toStringTag] = '@libp2p/address-manager' _updatePeerStoreAddresses (): void { // if announce addresses have been configured, ensure they make it into our peer // record for things like identify const addrs = this.getAddresses() .map(ma => { // strip our peer id if it is present if (ma.getPeerId() === this.components.peerId.toString()) { return ma.decapsulate(`/p2p/${this.components.peerId.toString()}`) } return ma }) this.components.peerStore.patch(this.components.peerId, { multiaddrs: addrs }) .catch(err => { this.log.error('error updating addresses', err) }) } /** * Get peer listen multiaddrs */ getListenAddrs (): Multiaddr[] { return Array.from(this.listen).map((a) => multiaddr(a)) } /** * Get peer announcing multiaddrs */ getAnnounceAddrs (): Multiaddr[] { return Array.from(this.announce).map((a) => multiaddr(a)) } /** * Get peer announcing multiaddrs */ getAppendAnnounceAddrs (): Multiaddr[] { return Array.from(this.appendAnnounce).map((a) => multiaddr(a)) } /** * Get observed multiaddrs */ getObservedAddrs (): Multiaddr[] { return this.observed.getAll().map(addr => addr.multiaddr) } /** * Add peer observed addresses */ addObservedAddr (addr: Multiaddr): void { const tuples = addr.stringTuples() const socketAddress = `${tuples[0][1]}:${tuples[1][1]}` // ignore if this address if it's been observed before if (this.observedAddressFilter.has(socketAddress)) { return } this.observedAddressFilter.add(socketAddress) addr = stripPeerId(addr, this.components.peerId) // ignore observed address if it is an IP mapping if (this.ipMappings.has(addr)) { return } // ignore observed address if it is a DNS mapping if (this.dnsMappings.has(addr)) { return } this.observed.add(addr) } confirmObservedAddr (addr: Multiaddr, options?: ConfirmAddressOptions): void { addr = stripPeerId(addr, this.components.peerId) let startingConfidence = true if (options?.type === 'transport' || this.transportAddresses.has(addr)) { const transportStartingConfidence = this.transportAddresses.confirm(addr, options?.ttl ?? this.addressVerificationTTL) if (!transportStartingConfidence && startingConfidence) { startingConfidence = false } } if (options?.type === 'dns-mapping' || this.dnsMappings.has(addr)) { const dnsMappingStartingConfidence = this.dnsMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) if (!dnsMappingStartingConfidence && startingConfidence) { startingConfidence = false } } if (options?.type === 'ip-mapping' || this.ipMappings.has(addr)) { const ipMappingStartingConfidence = this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) if (!ipMappingStartingConfidence && startingConfidence) { startingConfidence = false } } if (options?.type === 'observed' || this.observed.has(addr)) { // try to match up observed address with local transport listener if (this.maybeUpgradeToIPMapping(addr)) { this.ipMappings.confirm(addr, options?.ttl ?? this.addressVerificationTTL) startingConfidence = false } else { const observedStartingConfidence = this.observed.confirm(addr, options?.ttl ?? this.addressVerificationTTL) if (!observedStartingConfidence && startingConfidence) { startingConfidence = false } } } // only trigger the 'self:peer:update' event if our confidence in an address has changed if (!startingConfidence) { this._updatePeerStoreAddresses() } } removeObservedAddr (addr: Multiaddr, options?: ConfirmAddressOptions): void { addr = stripPeerId(addr, this.components.peerId) let startingConfidence = false if (this.observed.has(addr)) { const observedStartingConfidence = this.observed.remove(addr) if (!observedStartingConfidence && startingConfidence) { startingConfidence = false } } if (this.transportAddresses.has(addr)) { const transportStartingConfidence = this.transportAddresses.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) if (!transportStartingConfidence && startingConfidence) { startingConfidence = false } } if (this.dnsMappings.has(addr)) { const dnsMappingStartingConfidence = this.dnsMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) if (!dnsMappingStartingConfidence && startingConfidence) { startingConfidence = false } } if (this.ipMappings.has(addr)) { const ipMappingStartingConfidence = this.ipMappings.unconfirm(addr, options?.ttl ?? this.addressVerificationRetry) if (!ipMappingStartingConfidence && startingConfidence) { startingConfidence = false } } // only trigger the 'self:peer:update' event if our confidence in an address has changed if (startingConfidence) { this._updatePeerStoreAddresses() } } getAddresses (): Multiaddr[] { const addresses = new Set<string>() const multiaddrs = this.getAddressesWithMetadata() .filter(addr => { if (!addr.verified) { return false } const maStr = addr.multiaddr.toString() if (addresses.has(maStr)) { return false } addresses.add(maStr) return true }) .map(address => address.multiaddr) // filter addressees before returning return this.announceFilter( multiaddrs.map(str => { const ma = multiaddr(str) const lastComponent = ma.getComponents().pop() if (lastComponent?.value === this.components.peerId.toString()) { return ma } return ma.encapsulate(`/p2p/${this.components.peerId.toString()}`) }) ) } getAddressesWithMetadata (): NodeAddress[] { const announceMultiaddrs = this.getAnnounceAddrs() if (announceMultiaddrs.length > 0) { // allow transports to add certhashes and other runtime information this.components.transportManager.getListeners().forEach(listener => { listener.updateAnnounceAddrs(announceMultiaddrs) }) return announceMultiaddrs.map(multiaddr => ({ multiaddr, verified: true, type: 'announce', expires: Date.now() + this.addressVerificationTTL, lastVerified: Date.now() })) } let addresses: NodeAddress[] = [] // add transport addresses addresses = addresses.concat( this.components.transportManager.getAddrs() .map(multiaddr => this.transportAddresses.get(multiaddr, this.addressVerificationTTL)) ) const appendAnnounceMultiaddrs = this.getAppendAnnounceAddrs() // add append announce addresses if (appendAnnounceMultiaddrs.length > 0) { // allow transports to add certhashes and other runtime information this.components.transportManager.getListeners().forEach(listener => { listener.updateAnnounceAddrs(appendAnnounceMultiaddrs) }) addresses = addresses.concat( appendAnnounceMultiaddrs.map(multiaddr => ({ multiaddr, verified: true, type: 'announce', expires: Date.now() + this.addressVerificationTTL, lastVerified: Date.now() })) ) } // add observed addresses addresses = addresses.concat( this.observed.getAll() ) // add ip mapped addresses addresses = addresses.concat( this.ipMappings.getAll(addresses) ) // add ip->domain mappings, must be done after IP mappings addresses = addresses.concat( this.dnsMappings.getAll(addresses) ) return addresses } addDNSMapping (domain: string, addresses: string[]): void { this.dnsMappings.add(domain, addresses) } removeDNSMapping (domain: string): void { if (this.dnsMappings.remove(multiaddr(`/dns/${domain}`))) { this._updatePeerStoreAddresses() } } addPublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void { this.ipMappings.add(internalIp, internalPort, externalIp, externalPort, protocol) // remove duplicate observed addresses this.observed.removePrefixed(`/ip${isIPv4(externalIp) ? 4 : 6}/${externalIp}/${protocol}/${externalPort}`) } removePublicAddressMapping (internalIp: string, internalPort: number, externalIp: string, externalPort: number = internalPort, protocol: 'tcp' | 'udp' = 'tcp'): void { if (this.ipMappings.remove(multiaddr(`/ip${isIPv4(externalIp) ? 4 : 6}/${externalIp}/${protocol}/${externalPort}`))) { this._updatePeerStoreAddresses() } } /** * Where an external service (router, gateway, etc) is forwarding traffic to * us, attempt to add an IP mapping for the external address - this will * include the observed mapping in the address list where we also have a DNS * mapping for the external IP. * * Returns true if we added a new mapping */ private maybeUpgradeToIPMapping (ma: Multiaddr): boolean { // this address is already mapped if (this.ipMappings.has(ma)) { return false } const maOptions = ma.toOptions() // only public IPv4 addresses if (maOptions.family === 6 || maOptions.host === '127.0.0.1' || isPrivateIp(maOptions.host) === true) { return false } const listeners = this.components.transportManager.getListeners() const transportMatchers: Array<(ma: Multiaddr) => boolean> = [ (ma: Multiaddr) => WebSockets.exactMatch(ma) || WebSocketsSecure.exactMatch(ma), (ma: Multiaddr) => TCP.exactMatch(ma), (ma: Multiaddr) => QUIC_V1.exactMatch(ma) ] for (const matcher of transportMatchers) { // is the incoming address the same type as the matcher if (!matcher(ma)) { continue } // get the listeners for this transport const transportListeners = listeners.filter(listener => { return listener.getAddrs().filter(ma => { // only IPv4 addresses of the matcher type return ma.toOptions().family === 4 && matcher(ma) }).length > 0 }) // because the NAT mapping could be forwarding different external ports to // internal ones, we can only be sure enough to add a mapping if there is // a single listener if (transportListeners.length !== 1) { continue } // we have one listener which listens on one port so whatever the external // NAT port mapping is, it should be for this listener const linkLocalAddr = transportListeners[0].getAddrs().filter(ma => { return ma.toOptions().host !== '127.0.0.1' }).pop() if (linkLocalAddr == null) { continue } const linkLocalOptions = linkLocalAddr.toOptions() // upgrade observed address to IP mapping this.observed.remove(ma) this.ipMappings.add( linkLocalOptions.host, linkLocalOptions.port, maOptions.host, maOptions.port, maOptions.transport ) return true } return false } }