diff --git a/packages/interface-internal/src/address-manager/index.ts b/packages/interface-internal/src/address-manager/index.ts index 89b4231cca..9d055d5f65 100644 --- a/packages/interface-internal/src/address-manager/index.ts +++ b/packages/interface-internal/src/address-manager/index.ts @@ -40,4 +40,17 @@ export interface AddressManager { * Get the current node's addresses */ getAddresses(): Multiaddr[] + + /** + * Adds a mapping between one or more IP addresses and a domain name - when + * `getAddresses` is invoked, where the IP addresses are present in a + * multiaddr, an additional multiaddr will be added with `ip4` and `ip6` + * tuples replaced with `dns4` and `dns6 ones respectively. + */ + addDNSMapping(domain: string, ipAddresses: string[]): void + + /** + * Remove a mapping previously added with `addDNSMapping`. + */ + removeDNSMapping(domain: string): void } diff --git a/packages/libp2p/src/address-manager/index.ts b/packages/libp2p/src/address-manager.ts similarity index 65% rename from packages/libp2p/src/address-manager/index.ts rename to packages/libp2p/src/address-manager.ts index f7e076bf0e..0566db59d7 100644 --- a/packages/libp2p/src/address-manager/index.ts +++ b/packages/libp2p/src/address-manager.ts @@ -1,8 +1,8 @@ import { peerIdFromString } from '@libp2p/peer-id' -import { multiaddr } from '@multiformats/multiaddr' -import { debounce } from './utils.js' +import { debounce } from '@libp2p/utils/debounce' +import { multiaddr, protocols } from '@multiformats/multiaddr' import type { ComponentLogger, Libp2pEvents, Logger, TypedEventTarget, PeerId, PeerStore } from '@libp2p/interface' -import type { TransportManager } from '@libp2p/interface-internal' +import type { AddressManager as AddressManagerInterface, TransportManager } from '@libp2p/interface-internal' import type { Multiaddr } from '@multiformats/multiaddr' export interface AddressManagerInit { @@ -28,7 +28,7 @@ export interface AddressManagerInit { noAnnounce?: string[] } -export interface DefaultAddressManagerComponents { +export interface AddressManagerComponents { peerId: PeerId transportManager: TransportManager peerStore: PeerStore @@ -69,14 +69,22 @@ function stripPeerId (ma: Multiaddr, peerId: PeerId): Multiaddr { return ma } -export class DefaultAddressManager { +const CODEC_IP4 = 0x04 +const CODEC_IP6 = 0x29 +const CODEC_DNS4 = 0x36 +const CODEC_DNS6 = 0x37 + +export class AddressManager implements AddressManagerInterface { private readonly log: Logger - private readonly components: DefaultAddressManagerComponents + 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 private readonly observed: Map private readonly announceFilter: AddressFilter + private readonly ipDomainMappings: Map + + private readonly where: Error /** * Responsible for managing the peer addresses. @@ -84,7 +92,7 @@ export class DefaultAddressManager { * 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: DefaultAddressManagerComponents, init: AddressManagerInit = {}) { + constructor (components: AddressManagerComponents, init: AddressManagerInit = {}) { const { listen = [], announce = [] } = init this.components = components @@ -92,6 +100,7 @@ export class DefaultAddressManager { this.listen = listen.map(ma => ma.toString()) this.announce = new Set(announce.map(ma => ma.toString())) this.observed = new Map() + this.ipDomainMappings = new Map() this.announceFilter = init.announceFilter ?? defaultAddressFilter // this method gets called repeatedly on startup when transports start listening so @@ -106,6 +115,8 @@ export class DefaultAddressManager { components.events.addEventListener('transport:close', () => { this._updatePeerStoreAddresses() }) + + this.where = new Error('where') } readonly [Symbol.toStringTag] = '@libp2p/address-manager' @@ -200,37 +211,109 @@ export class DefaultAddressManager { } getAddresses (): Multiaddr[] { - let addrs = this.getAnnounceAddrs().map(ma => ma.toString()) + let multiaddrs = this.getAnnounceAddrs() - if (addrs.length === 0) { + if (multiaddrs.length === 0) { // no configured announce addrs, add configured listen addresses - addrs = this.components.transportManager.getAddrs().map(ma => ma.toString()) + multiaddrs = this.components.transportManager.getAddrs() } // add observed addresses we are confident in - addrs = addrs.concat( - Array.from(this.observed) - .filter(([ma, metadata]) => metadata.confident) - .map(([ma]) => ma) - ) + multiaddrs = multiaddrs + .concat( + Array.from(this.observed) + .filter(([ma, metadata]) => metadata.confident) + .map(([ma]) => multiaddr(ma)) + ) + + const mappedMultiaddrs: Multiaddr[] = [] + + // add ip->domain mappings + for (const ma of multiaddrs) { + const tuples = [...ma.stringTuples()] + let mappedIp = false + + for (const [ip, domain] of this.ipDomainMappings.entries()) { + for (let i = 0; i < tuples.length; i++) { + if (tuples[i][1] !== ip) { + continue + } + + if (tuples[i][0] === CODEC_IP4) { + tuples[i][0] = CODEC_DNS4 + tuples[i][1] = domain + mappedIp = true + } + + if (tuples[i][0] === CODEC_IP6) { + tuples[i][0] = CODEC_DNS6 + tuples[i][1] = domain + mappedIp = true + } + } + } + + if (mappedIp) { + mappedMultiaddrs.push( + multiaddr(`/${ + tuples.map(tuple => { + return [ + protocols(tuple[0]).name, + tuple[1] + ].join('/') + }).join('/') + }`) + ) + } + } + + multiaddrs = multiaddrs.concat(mappedMultiaddrs) // dedupe multiaddrs - const addrSet = new Set(addrs) + const addrSet = new Set() + multiaddrs = multiaddrs.filter(ma => { + const maStr = ma.toString() + + if (addrSet.has(maStr)) { + return false + } + + addrSet.add(maStr) + + return true + }) // Create advertising list - return this.announceFilter(Array.from(addrSet) - .map(str => multiaddr(str))) - .map(ma => { - // do not append our peer id to a path multiaddr as it will become invalid - if (ma.protos().pop()?.path === true) { - return ma - } + return this.announceFilter( + Array.from(addrSet) + .map(str => { + const ma = multiaddr(str) + + // do not append our peer id to a path multiaddr as it will become invalid + if (ma.protos().pop()?.path === true) { + return ma + } + + if (ma.getPeerId() === this.components.peerId.toString()) { + return ma + } + + return ma.encapsulate(`/p2p/${this.components.peerId.toString()}`) + }) + ) + } - if (ma.getPeerId() === this.components.peerId.toString()) { - return ma - } + addDNSMapping (domain: string, addresses: string[]): void { + addresses.forEach(ip => { + this.ipDomainMappings.set(ip, domain) + }) + } - return ma.encapsulate(`/p2p/${this.components.peerId.toString()}`) - }) + removeDNSMapping (domain: string): void { + for (const [key, value] of this.ipDomainMappings.entries()) { + if (value === domain) { + this.ipDomainMappings.delete(key) + } + } } } diff --git a/packages/libp2p/src/address-manager/README.md b/packages/libp2p/src/address-manager/README.md deleted file mode 100644 index 792789275d..0000000000 --- a/packages/libp2p/src/address-manager/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Address Manager - -The Address manager is responsible for keeping an updated register of the peer's addresses. It includes 2 different types of Addresses: `Listen Addresses` and `Announce Addresses`. - -These Addresses should be specified in your libp2p [configuration](../../../../doc/CONFIGURATION.md) when you create your node. - -## Listen Addresses - -A libp2p node should have a set of listen addresses, which will be used by libp2p underlying transports to listen for dials from other nodes in the network. - -Before a libp2p node starts, its configured listen addresses will be passed to the AddressManager, so that during startup the libp2p transports can use them to listen for connections. Accordingly, listen addresses should be specified through the libp2p configuration, in order to have the `AddressManager` created with them. - -It is important pointing out that libp2p accepts ephemeral listening addresses. In this context, the provided listen addresses might not be exactly the same as the ones used by the transports. For example TCP may replace `/ip4/0.0.0.0/tcp/0` with something like `/ip4/127.0.0.1/tcp/8989`. As a consequence, libp2p should take into account this when determining its advertised addresses. - -## Announce Addresses - -In some scenarios, a libp2p node will need to announce addresses that it is not listening on. In other words, Announce Addresses are an amendment to the Listen Addresses that aim to enable other nodes to achieve connectivity to this node. - -Scenarios for Announce Addresses include: -- when you setup a libp2p node in your private network at home, but you need to announce your public IP Address to the outside world; -- when you want to announce a DNS address, which maps to your public IP Address. - -## Implementation - -When a libp2p node is created, the Address Manager will be populated from the provided addresses through the libp2p configuration. Once the node is started, the Transport Manager component will gather the listen addresses from the Address Manager, so that the libp2p transports can attempt to bind to them. - -Libp2p will use the Address Manager as the source of truth when advertising the peers addresses. After all transports are ready, other libp2p components/subsystems will kickoff, namely the Identify Service and the DHT. Both of them will announce the node addresses to the other peers in the network. The announce addresses will have an important role here and will be gathered by libp2p to compute its current addresses to advertise everytime it is needed. - -## Future Considerations - -### Dynamic address modifications - -In a future iteration, we can enable these addresses to be modified in runtime. For this, the Address Manager should be responsible for notifying interested subsystems of these changes, through an Event Emitter. - -#### Modify Listen Addresses - -While adding new addresses to listen on runtime should be trivial, removing a listen address might have bad implications for the node, since all the connections using that listen address will be closed. However, libp2p should provide a mechanism for both adding and removing listen addresses in the future. - -Every time a new listen address is added, the Address Manager should emit an event with the new multiaddrs to listen. The Transport Manager should listen to this events and act accordingly. - -#### Modify Announce Addresses - -When the announce addresses are modified, the Address Manager should emit an event so that other subsystems can act accordingly. For example, libp2p identify service should use the libp2p push protocol to inform other peers about these changes. diff --git a/packages/libp2p/src/address-manager/utils.ts b/packages/libp2p/src/address-manager/utils.ts deleted file mode 100644 index 7062446a86..0000000000 --- a/packages/libp2p/src/address-manager/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function debounce (func: () => void, wait: number): () => void { - let timeout: ReturnType | undefined - - return function () { - const later = function (): void { - timeout = undefined - func() - } - - clearTimeout(timeout) - timeout = setTimeout(later, wait) - } -} diff --git a/packages/libp2p/src/index.ts b/packages/libp2p/src/index.ts index fc68bc0bdf..d05b2883c6 100644 --- a/packages/libp2p/src/index.ts +++ b/packages/libp2p/src/index.ts @@ -18,7 +18,7 @@ import { generateKeyPair } from '@libp2p/crypto/keys' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { validateConfig } from './config.js' import { Libp2p as Libp2pClass } from './libp2p.js' -import type { AddressManagerInit, AddressFilter } from './address-manager/index.js' +import type { AddressManagerInit, AddressFilter } from './address-manager.js' import type { Components } from './components.js' import type { ConnectionManagerInit } from './connection-manager/index.js' import type { ConnectionMonitorInit } from './connection-monitor.js' diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 912afeb716..b88e29dab9 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -8,7 +8,7 @@ import { isMultiaddr, type Multiaddr } from '@multiformats/multiaddr' import { MemoryDatastore } from 'datastore-core/memory' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { DefaultAddressManager } from './address-manager/index.js' +import { AddressManager } from './address-manager.js' import { checkServiceDependencies, defaultComponents } from './components.js' import { connectionGater } from './config/connection-gater.js' import { DefaultConnectionManager } from './connection-manager/index.js' @@ -129,7 +129,7 @@ export class Libp2p extends TypedEventEmitter this.configureComponent('registrar', new DefaultRegistrar(this.components)) // Addresses {listen, announce, noAnnounce} - this.configureComponent('addressManager', new DefaultAddressManager(this.components, init.addresses)) + this.configureComponent('addressManager', new AddressManager(this.components, init.addresses)) // Peer routers const peerRouters: PeerRouting[] = (init.peerRouters ?? []).map((fn, index) => this.configureComponent(`peer-router-${index}`, fn(this.components))) diff --git a/packages/libp2p/test/addresses/address-manager.spec.ts b/packages/libp2p/test/addresses/address-manager.spec.ts index dda164c932..d2069a5864 100644 --- a/packages/libp2p/test/addresses/address-manager.spec.ts +++ b/packages/libp2p/test/addresses/address-manager.spec.ts @@ -1,7 +1,7 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore } from '@libp2p/interface' +import { TypedEventEmitter, type TypedEventTarget, type Libp2pEvents, type PeerId, type PeerStore, type Peer } from '@libp2p/interface' import { defaultLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' @@ -9,7 +9,7 @@ import { expect } from 'aegir/chai' import delay from 'delay' import Sinon from 'sinon' import { type StubbedInstance, stubInterface } from 'sinon-ts' -import { type AddressFilter, DefaultAddressManager } from '../../src/address-manager/index.js' +import { type AddressFilter, AddressManager } from '../../src/address-manager.js' import type { TransportManager } from '@libp2p/interface-internal' const listenAddresses = ['/ip4/127.0.0.1/tcp/15006/ws', '/ip4/127.0.0.1/tcp/15008/ws'] @@ -23,13 +23,13 @@ describe('Address Manager', () => { beforeEach(async () => { peerId = peerIdFromPrivateKey(await generateKeyPair('Ed25519')) peerStore = stubInterface({ - patch: Sinon.stub().resolves({}) + patch: Sinon.stub().resolves(stubInterface()) }) events = new TypedEventEmitter() }) it('should not need any addresses', () => { - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -44,7 +44,7 @@ describe('Address Manager', () => { }) it('should return listen multiaddrs on get', () => { - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -65,7 +65,7 @@ describe('Address Manager', () => { }) it('should return announce multiaddrs on get', () => { - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -86,7 +86,7 @@ describe('Address Manager', () => { }) it('should add observed addresses', () => { - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -105,7 +105,7 @@ describe('Address Manager', () => { it('should allow duplicate listen addresses', () => { const ma = multiaddr('/ip4/0.0.0.0/tcp/0') - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -127,7 +127,7 @@ describe('Address Manager', () => { it('should dedupe added observed addresses', () => { const ma = multiaddr('/ip4/123.123.123.123/tcp/39201') - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -149,7 +149,7 @@ describe('Address Manager', () => { it('should only set addresses once', async () => { const ma = '/ip4/123.123.123.123/tcp/39201' - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface({ getAddrs: Sinon.stub().returns([]) @@ -172,7 +172,7 @@ describe('Address Manager', () => { it('should strip our peer address from added observed addresses', () => { const ma = multiaddr('/ip4/123.123.123.123/tcp/39201') - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -191,7 +191,7 @@ describe('Address Manager', () => { it('should strip our peer address from added observed addresses in difference formats', () => { const ma = multiaddr('/ip4/123.123.123.123/tcp/39201') - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager: stubInterface(), peerStore, @@ -211,7 +211,7 @@ describe('Address Manager', () => { it('should not add our peer id to path multiaddrs', () => { const ma = '/unix/foo/bar/baz' const transportManager = stubInterface() - const am = new DefaultAddressManager({ + const am = new AddressManager({ peerId, transportManager, peerStore, @@ -228,4 +228,98 @@ describe('Address Manager', () => { expect(addrs).to.have.lengthOf(1) expect(addrs[0].toString()).to.not.include(`/p2p/${peerId.toString()}`) }) + + it('should add an IPv4 DNS mapping', () => { + const am = new AddressManager({ + peerId, + transportManager: stubInterface({ + getAddrs: () => [] + }), + peerStore, + events, + logger: defaultLogger() + }) + + expect(am.getAddresses()).to.be.empty() + + const externalIp = '81.12.12.1' + const externalAddress = multiaddr(`/ip4/${externalIp}/tcp/1234`) + + am.confirmObservedAddr(externalAddress) + + expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)]) + + const domain = 'example.com' + + am.addDNSMapping(domain, [externalIp]) + + expect(am.getAddresses()).to.deep.equal([ + externalAddress.encapsulate(`/p2p/${peerId.toString()}`), + multiaddr(`/dns4/${domain}/tcp/1234/p2p/${peerId.toString()}`) + ]) + }) + + it('should add an IPv6 DNS mapping', () => { + const am = new AddressManager({ + peerId, + transportManager: stubInterface({ + getAddrs: () => [] + }), + peerStore, + events, + logger: defaultLogger() + }) + + expect(am.getAddresses()).to.be.empty() + + const externalIp = 'fe80::7c98:a9ff:fe94' + const externalAddress = multiaddr(`/ip6/${externalIp}/tcp/1234`) + + am.confirmObservedAddr(externalAddress) + + expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)]) + + const domain = 'example.com' + + am.addDNSMapping(domain, [externalIp]) + + expect(am.getAddresses()).to.deep.equal([ + externalAddress.encapsulate(`/p2p/${peerId.toString()}`), + multiaddr(`/dns6/${domain}/tcp/1234/p2p/${peerId.toString()}`) + ]) + }) + + it('should remove add a DNS mapping', () => { + const am = new AddressManager({ + peerId, + transportManager: stubInterface({ + getAddrs: () => [] + }), + peerStore, + events, + logger: defaultLogger() + }) + + expect(am.getAddresses()).to.be.empty() + + const externalIp = '81.12.12.1' + const externalAddress = multiaddr(`/ip4/${externalIp}/tcp/1234`) + + am.confirmObservedAddr(externalAddress) + + expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)]) + + const domain = 'example.com' + + am.addDNSMapping(domain, [externalIp]) + + expect(am.getAddresses()).to.deep.equal([ + externalAddress.encapsulate(`/p2p/${peerId.toString()}`), + multiaddr(`/dns4/${domain}/tcp/1234/p2p/${peerId.toString()}`) + ]) + + am.removeDNSMapping(domain) + + expect(am.getAddresses()).to.deep.equal([externalAddress.encapsulate(`/p2p/${peerId.toString()}`)]) + }) }) diff --git a/packages/libp2p/test/connection-manager/dial-queue.spec.ts b/packages/libp2p/test/connection-manager/dial-queue.spec.ts index c5903670bc..1534b5b438 100644 --- a/packages/libp2p/test/connection-manager/dial-queue.spec.ts +++ b/packages/libp2p/test/connection-manager/dial-queue.spec.ts @@ -43,8 +43,6 @@ describe('dial queue', () => { if (dialer != null) { dialer.stop() } - - sinon.reset() }) it('should end when a single multiaddr dials succeeds', async () => { diff --git a/packages/libp2p/test/connection-manager/reconnect-queue.spec.ts b/packages/libp2p/test/connection-manager/reconnect-queue.spec.ts index a7448cc3d3..85993b029f 100644 --- a/packages/libp2p/test/connection-manager/reconnect-queue.spec.ts +++ b/packages/libp2p/test/connection-manager/reconnect-queue.spec.ts @@ -7,7 +7,7 @@ import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { expect } from 'aegir/chai' import delay from 'delay' import pRetry from 'p-retry' -import sinon from 'sinon' +import Sinon from 'sinon' import { type StubbedInstance, stubInterface } from 'sinon-ts' import { ReconnectQueue } from '../../src/connection-manager/reconnect-queue.js' import type { ComponentLogger, Libp2pEvents, PeerStore, TypedEventTarget, Peer } from '@libp2p/interface' @@ -28,15 +28,15 @@ describe('reconnect queue', () => { components = { connectionManager: stubInterface(), events: new TypedEventEmitter(), - peerStore: stubInterface(), + peerStore: stubInterface({ + all: Sinon.stub().resolves([]) + }), logger: peerLogger(peerId) } }) afterEach(async () => { await stop(queue) - - sinon.reset() }) it('should reconnect to KEEP_ALIVE peers on startup', async () => { diff --git a/packages/libp2p/test/peer-discovery/peer-discovery.spec.ts b/packages/libp2p/test/peer-discovery/peer-discovery.spec.ts index 847cc96f3a..20f16f4216 100644 --- a/packages/libp2p/test/peer-discovery/peer-discovery.spec.ts +++ b/packages/libp2p/test/peer-discovery/peer-discovery.spec.ts @@ -15,8 +15,6 @@ describe('peer discovery', () => { if (libp2p != null) { await libp2p.stop() } - - sinon.reset() }) it('should start/stop startable discovery on libp2p start/stop', async () => { diff --git a/packages/libp2p/test/transports/transport-manager.spec.ts b/packages/libp2p/test/transports/transport-manager.spec.ts index f93ec03610..2ee34f1b69 100644 --- a/packages/libp2p/test/transports/transport-manager.spec.ts +++ b/packages/libp2p/test/transports/transport-manager.spec.ts @@ -12,7 +12,7 @@ import { pEvent } from 'p-event' import pWaitFor from 'p-wait-for' import Sinon from 'sinon' import { stubInterface } from 'sinon-ts' -import { DefaultAddressManager } from '../../src/address-manager/index.js' +import { AddressManager } from '../../src/address-manager.js' import { DefaultTransportManager } from '../../src/transport-manager.js' import type { Components } from '../../src/components.js' import type { Connection, Transport, Upgrader, Listener } from '@libp2p/interface' @@ -41,7 +41,7 @@ describe('Transport Manager', () => { logger: defaultLogger(), datastore: new MemoryDatastore() } as any - components.addressManager = new DefaultAddressManager(components, { listen: [listenAddr.toString()] }) + components.addressManager = new AddressManager(components, { listen: [listenAddr.toString()] }) components.peerStore = persistentPeerStore(components) components.transportManager = tm = new DefaultTransportManager(components, {