UNPKG

libp2p

Version:

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

363 lines • 16 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'; const ONE_MINUTE = 60_000; export const defaultValues = { maxObservedAddresses: 10, addressVerificationTTL: ONE_MINUTE * 10, addressVerificationRetry: ONE_MINUTE * 5 }; const defaultAddressFilter = (addrs) => addrs; /** * If the passed multiaddr contains the passed peer id, remove it */ function stripPeerId(ma, peerId) { 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 { log; components; // this is an array to allow for duplicates, e.g. multiples of `/ip4/0.0.0.0/tcp/0` listen; announce; appendAnnounce; announceFilter; observed; dnsMappings; ipMappings; transportAddresses; observedAddressFilter; addressVerificationTTL; addressVerificationRetry; /** * 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, init = {}) { 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(); }); } [Symbol.toStringTag] = '@libp2p/address-manager'; _updatePeerStoreAddresses() { // 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() { return Array.from(this.listen).map((a) => multiaddr(a)); } /** * Get peer announcing multiaddrs */ getAnnounceAddrs() { return Array.from(this.announce).map((a) => multiaddr(a)); } /** * Get peer announcing multiaddrs */ getAppendAnnounceAddrs() { return Array.from(this.appendAnnounce).map((a) => multiaddr(a)); } /** * Get observed multiaddrs */ getObservedAddrs() { return this.observed.getAll().map(addr => addr.multiaddr); } /** * Add peer observed addresses */ addObservedAddr(addr) { 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, options) { 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, options) { 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() { const addresses = new Set(); 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() { 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 = []; // 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, addresses) { this.dnsMappings.add(domain, addresses); } removeDNSMapping(domain) { if (this.dnsMappings.remove(multiaddr(`/dns/${domain}`))) { this._updatePeerStoreAddresses(); } } addPublicAddressMapping(internalIp, internalPort, externalIp, externalPort = internalPort, protocol = 'tcp') { 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, internalPort, externalIp, externalPort = internalPort, protocol = 'tcp') { 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 */ maybeUpgradeToIPMapping(ma) { // 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 = [ (ma) => WebSockets.exactMatch(ma) || WebSocketsSecure.exactMatch(ma), (ma) => TCP.exactMatch(ma), (ma) => 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; } } //# sourceMappingURL=index.js.map