From 7626b224d23c474d1c885c8a7922977ab7e4bea6 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 12 Nov 2024 09:17:04 +0000 Subject: [PATCH 1/2] fix: run UPnP nat on address change, update nat port mapper (#2797) - Updates NAT port mapper to handle unavailable ports - Runs NAT port mapping on address change rather than once at startup - Re-maps addresses when the public IP address changes --- packages/upnp-nat/package.json | 7 +- .../upnp-nat/src/check-external-address.ts | 139 ++++++++++ packages/upnp-nat/src/errors.ts | 5 + packages/upnp-nat/src/index.ts | 61 ++++- packages/upnp-nat/src/upnp-nat.ts | 238 ++++++++++++------ packages/upnp-nat/test/index.spec.ts | 79 ++++-- 6 files changed, 433 insertions(+), 96 deletions(-) create mode 100644 packages/upnp-nat/src/check-external-address.ts diff --git a/packages/upnp-nat/package.json b/packages/upnp-nat/package.json index 2924d20a79..36e588bf3d 100644 --- a/packages/upnp-nat/package.json +++ b/packages/upnp-nat/package.json @@ -50,12 +50,15 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@achingbrain/nat-port-mapper": "^1.0.13", + "@achingbrain/nat-port-mapper": "^2.0.1", + "@chainsafe/is-ip": "^2.0.2", "@libp2p/interface": "^2.2.0", "@libp2p/interface-internal": "^2.0.10", "@libp2p/utils": "^6.1.3", "@multiformats/multiaddr": "^12.2.3", - "wherearewe": "^2.0.1" + "@multiformats/multiaddr-matcher": "^1.4.0", + "p-defer": "^4.0.1", + "race-signal": "^1.1.0" }, "devDependencies": { "@libp2p/crypto": "^5.0.6", diff --git a/packages/upnp-nat/src/check-external-address.ts b/packages/upnp-nat/src/check-external-address.ts new file mode 100644 index 0000000000..bad9f14d3c --- /dev/null +++ b/packages/upnp-nat/src/check-external-address.ts @@ -0,0 +1,139 @@ +import { NotStartedError, start, stop } from '@libp2p/interface' +import { repeatingTask } from '@libp2p/utils/repeating-task' +import { multiaddr } from '@multiformats/multiaddr' +import pDefer from 'p-defer' +import { raceSignal } from 'race-signal' +import type { NatAPI } from '@achingbrain/nat-port-mapper' +import type { AbortOptions, ComponentLogger, Logger, Startable } from '@libp2p/interface' +import type { AddressManager } from '@libp2p/interface-internal' +import type { RepeatingTask } from '@libp2p/utils/repeating-task' +import type { DeferredPromise } from 'p-defer' + +export interface ExternalAddressCheckerComponents { + client: NatAPI + addressManager: AddressManager + logger: ComponentLogger +} + +export interface ExternalAddressCheckerInit { + interval?: number + timeout?: number + autoConfirmAddress?: boolean +} + +export interface ExternalAddress { + getPublicIp (options?: AbortOptions): Promise | string +} + +/** + * Monitors the external network address and notifies when/if it changes + */ +class ExternalAddressChecker implements ExternalAddress, Startable { + private readonly log: Logger + private readonly client: NatAPI + private readonly addressManager: AddressManager + private started: boolean + private lastPublicIp?: string + private readonly lastPublicIpPromise: DeferredPromise + private readonly check: RepeatingTask + private readonly autoConfirmAddress: boolean + + constructor (components: ExternalAddressCheckerComponents, init: ExternalAddressCheckerInit = {}) { + this.log = components.logger.forComponent('libp2p:upnp-nat:external-address-check') + this.client = components.client + this.addressManager = components.addressManager + this.autoConfirmAddress = init.autoConfirmAddress ?? false + this.started = false + + this.checkExternalAddress = this.checkExternalAddress.bind(this) + + this.lastPublicIpPromise = pDefer() + + this.check = repeatingTask(this.checkExternalAddress, init.interval ?? 30000, { + timeout: init.timeout ?? 10000, + runImmediately: true + }) + } + + async start (): Promise { + if (this.started) { + return + } + + await start(this.check) + + this.check.start() + this.started = true + } + + async stop (): Promise { + await stop(this.check) + + this.started = false + } + + /** + * Return the last public IP address we found, or wait for it to be found + */ + async getPublicIp (options?: AbortOptions): Promise { + if (!this.started) { + throw new NotStartedError('Not started yet') + } + + return this.lastPublicIp ?? raceSignal(this.lastPublicIpPromise.promise, options?.signal, { + errorMessage: 'Requesting the public IP from the network gateway timed out - UPnP may not be enabled' + }) + } + + private async checkExternalAddress (options?: AbortOptions): Promise { + try { + const externalAddress = await this.client.externalIp(options) + + // check if our public address has changed + if (this.lastPublicIp != null && externalAddress !== this.lastPublicIp) { + this.log('external address changed from %s to %s', this.lastPublicIp, externalAddress) + + for (const ma of this.addressManager.getAddresses()) { + const addrString = ma.toString() + + if (!addrString.includes(this.lastPublicIp)) { + continue + } + + // create a new version of the multiaddr with the new public IP + const newAddress = multiaddr(addrString.replace(this.lastPublicIp, externalAddress)) + + // remove the old address and add the new one + this.addressManager.removeObservedAddr(ma) + this.addressManager.confirmObservedAddr(newAddress) + + if (this.autoConfirmAddress) { + this.addressManager.confirmObservedAddr(newAddress) + } else { + this.addressManager.addObservedAddr(newAddress) + } + } + } + + this.lastPublicIp = externalAddress + this.lastPublicIpPromise.resolve(externalAddress) + } catch (err: any) { + if (this.lastPublicIp != null) { + // ignore the error if we've previously run successfully + return + } + + this.lastPublicIpPromise.reject(err) + } + } +} + +export function dynamicExternalAddress (components: ExternalAddressCheckerComponents, init: ExternalAddressCheckerInit = {}): ExternalAddress { + return new ExternalAddressChecker(components, init) +} + +export function staticExternalAddress (address: string): ExternalAddress { + return { + getPublicIp: () => address + } +} diff --git a/packages/upnp-nat/src/errors.ts b/packages/upnp-nat/src/errors.ts index 1ab9ce7562..02b4350200 100644 --- a/packages/upnp-nat/src/errors.ts +++ b/packages/upnp-nat/src/errors.ts @@ -4,3 +4,8 @@ export class DoubleNATError extends Error { this.name = 'DoubleNATError' } } + +export class InvalidIPAddressError extends Error { + static name = 'InvalidIPAddressError' + name = 'InvalidIPAddressError' +} diff --git a/packages/upnp-nat/src/index.ts b/packages/upnp-nat/src/index.ts index f22f1f200b..5168a601cd 100644 --- a/packages/upnp-nat/src/index.ts +++ b/packages/upnp-nat/src/index.ts @@ -36,8 +36,8 @@ */ import { UPnPNAT as UPnPNATClass, type NatAPI, type MapPortOptions } from './upnp-nat.js' -import type { ComponentLogger, NodeInfo, PeerId } from '@libp2p/interface' -import type { AddressManager, TransportManager } from '@libp2p/interface-internal' +import type { ComponentLogger, Libp2pEvents, NodeInfo, PeerId, TypedEventTarget } from '@libp2p/interface' +import type { AddressManager } from '@libp2p/interface-internal' export type { NatAPI, MapPortOptions } @@ -50,10 +50,27 @@ export interface PMPOptions { export interface UPnPNATInit { /** - * Pass a value to use instead of auto-detection + * Pass a string to hard code the external address, otherwise it will be + * auto-detected */ externalAddress?: string + /** + * Check if the external address has changed this often in ms. Ignored if an + * external address is specified. + * + * @default 30000 + */ + externalAddressCheckInterval?: number + + /** + * Do not take longer than this to check if the external address has changed + * in ms. Ignored if an external address is specified. + * + * @default 10000 + */ + externalAddressCheckTimeout?: number + /** * Pass a value to use instead of auto-detection */ @@ -78,14 +95,50 @@ export interface UPnPNATInit { * Pass a value to use instead of auto-detection */ gateway?: string + + /** + * How long in ms to wait before giving up trying to auto-detect a + * `urn:schemas-upnp-org:device:InternetGatewayDevice:1` device on the local + * network + * + * @default 10000 + */ + gatewayDetectionTimeout?: number + + /** + * Ports are mapped when the `self:peer:update` event fires, which happens + * when the node's addresses change. To avoid starting to map ports while + * multiple addresses are being added, the mapping function is debounced by + * this number of ms + * + * @default 5000 + */ + delay?: number + + /** + * A preconfigured instance of a NatAPI client can be passed as an option, + * otherwise one will be created + */ + client?: NatAPI + + /** + * Any mapped addresses are added to the observed address list. These + * addresses require additional verification by the `@libp2p/autonat` protocol + * or similar before they are trusted. + * + * To skip this verification and trust them immediately pass `true` here + * + * @default false + */ + autoConfirmAddress?: boolean } export interface UPnPNATComponents { peerId: PeerId nodeInfo: NodeInfo logger: ComponentLogger - transportManager: TransportManager addressManager: AddressManager + events: TypedEventTarget } export interface UPnPNAT { diff --git a/packages/upnp-nat/src/upnp-nat.ts b/packages/upnp-nat/src/upnp-nat.ts index e15091261a..1cd480141b 100644 --- a/packages/upnp-nat/src/upnp-nat.ts +++ b/packages/upnp-nat/src/upnp-nat.ts @@ -1,25 +1,32 @@ -import { upnpNat, type NatAPI, type MapPortOptions } from '@achingbrain/nat-port-mapper' -import { InvalidParametersError, serviceCapabilities } from '@libp2p/interface' +import { upnpNat } from '@achingbrain/nat-port-mapper' +import { isIPv4, isIPv6 } from '@chainsafe/is-ip' +import { InvalidParametersError, serviceCapabilities, start, stop } from '@libp2p/interface' +import { debounce } from '@libp2p/utils/debounce' import { isLoopback } from '@libp2p/utils/multiaddr/is-loopback' +import { isPrivate } from '@libp2p/utils/multiaddr/is-private' import { isPrivateIp } from '@libp2p/utils/private-ip' -import { fromNodeAddress } from '@multiformats/multiaddr' -import { isBrowser } from 'wherearewe' -import { DoubleNATError } from './errors.js' +import { multiaddr } from '@multiformats/multiaddr' +import { QUICV1, TCP, WebSockets, WebSocketsSecure, WebTransport } from '@multiformats/multiaddr-matcher' +import { raceSignal } from 'race-signal' +import { dynamicExternalAddress, staticExternalAddress } from './check-external-address.js' +import { DoubleNATError, InvalidIPAddressError } from './errors.js' +import type { ExternalAddress } from './check-external-address.js' import type { UPnPNATComponents, UPnPNATInit, UPnPNAT as UPnPNATInterface } from './index.js' -import type { Logger, Startable } from '@libp2p/interface' +import type { NatAPI, MapPortOptions } from '@achingbrain/nat-port-mapper' +import type { Libp2pEvents, Logger, Startable, TypedEventTarget } from '@libp2p/interface' +import type { AddressManager } from '@libp2p/interface-internal' +import type { DebouncedFunction } from '@libp2p/utils/debounce' +import type { Multiaddr } from '@multiformats/multiaddr' const DEFAULT_TTL = 7200 export type { NatAPI, MapPortOptions } -function highPort (min = 1024, max = 65535): number { - return Math.floor(Math.random() * (max - min + 1) + min) -} - export class UPnPNAT implements Startable, UPnPNATInterface { public client: NatAPI - private readonly components: UPnPNATComponents - private readonly externalAddress?: string + private readonly addressManager: AddressManager + private readonly events: TypedEventTarget + private readonly externalAddress: ExternalAddress private readonly localAddress?: string private readonly description: string private readonly ttl: number @@ -27,29 +34,51 @@ export class UPnPNAT implements Startable, UPnPNATInterface { private readonly gateway?: string private started: boolean private readonly log: Logger + private readonly gatewayDetectionTimeout: number + private readonly mappedPorts: Map + private readonly onSelfPeerUpdate: DebouncedFunction + private readonly autoConfirmAddress: boolean constructor (components: UPnPNATComponents, init: UPnPNATInit) { - this.components = components - this.log = components.logger.forComponent('libp2p:upnp-nat') + this.addressManager = components.addressManager + this.events = components.events this.started = false - this.externalAddress = init.externalAddress this.localAddress = init.localAddress - this.description = init.description ?? `${components.nodeInfo.name}@${components.nodeInfo.version} ${this.components.peerId.toString()}` + this.description = init.description ?? `${components.nodeInfo.name}@${components.nodeInfo.version} ${components.peerId.toString()}` this.ttl = init.ttl ?? DEFAULT_TTL this.keepAlive = init.keepAlive ?? true this.gateway = init.gateway + this.gatewayDetectionTimeout = init.gatewayDetectionTimeout ?? 10000 + this.autoConfirmAddress = init.autoConfirmAddress ?? false + this.mappedPorts = new Map() if (this.ttl < DEFAULT_TTL) { throw new InvalidParametersError(`NatManager ttl should be at least ${DEFAULT_TTL} seconds`) } - this.client = upnpNat({ + this.client = init.client ?? upnpNat({ description: this.description, ttl: this.ttl, keepAlive: this.keepAlive, gateway: this.gateway }) + + this.onSelfPeerUpdate = debounce(this._onSelfPeerUpdate.bind(this), init.delay ?? 5000) + + if (typeof init.externalAddress === 'string') { + this.externalAddress = staticExternalAddress(init.externalAddress) + } else { + this.externalAddress = dynamicExternalAddress({ + client: this.client, + addressManager: this.addressManager, + logger: components.logger + }, { + autoConfirmAddress: init.autoConfirmAddress, + interval: init.externalAddressCheckInterval, + timeout: init.externalAddressCheckTimeout + }) + } } readonly [Symbol.toStringTag] = '@libp2p/upnp-nat' @@ -62,95 +91,164 @@ export class UPnPNAT implements Startable, UPnPNATInterface { return this.started } - start (): void { - // #TODO: is there a way to remove this? Seems like a hack + async start (): Promise { + if (this.started) { + return + } + + this.started = true + this.events.addEventListener('self:peer:update', this.onSelfPeerUpdate) + await start(this.externalAddress, this.onSelfPeerUpdate) } /** - * Attempt to use uPnP to configure port mapping using the current gateway. - * - * Run after start to ensure the transport manager has all addresses configured. + * Stops the NAT manager */ - afterStart (): void { - if (isBrowser || this.started) { - return + async stop (): Promise { + try { + await this.client?.close() + } catch (err: any) { + this.log.error(err) } - this.started = true + this.events.removeEventListener('self:peer:update', this.onSelfPeerUpdate) + await stop(this.externalAddress, this.onSelfPeerUpdate) + this.started = false + } - // done async to not slow down startup - void this.mapIpAddresses().catch((err) => { - // hole punching errors are non-fatal - this.log.error(err) - }) + _onSelfPeerUpdate (): void { + this.mapIpAddresses() + .catch(err => { + this.log.error('error mapping IP addresses - %e', err) + }) } - async mapIpAddresses (): Promise { - const addrs = this.components.transportManager.getAddrs() + private getUnmappedAddresses (multiaddrs: Multiaddr[], ipType: 4 | 6): Multiaddr[] { + const output: Multiaddr[] = [] - for (const addr of addrs) { - // try to open uPnP ports for each thin waist address - const { family, host, port, transport } = addr.toOptions() + for (const ma of multiaddrs) { + // ignore public addresses + if (!isPrivate(ma)) { + continue + } + + // ignore loopback + if (isLoopback(ma)) { + continue + } - if (!addr.isThinWaistAddress() || transport !== 'tcp') { - // only bare tcp addresses - // eslint-disable-next-line no-continue + // only IP based addresses + if (!( + TCP.exactMatch(ma) || + WebSockets.exactMatch(ma) || + WebSocketsSecure.exactMatch(ma) || + QUICV1.exactMatch(ma) || + WebTransport.exactMatch(ma) + )) { continue } - if (isLoopback(addr)) { - // eslint-disable-next-line no-continue + const { port, family } = ma.toOptions() + + if (family !== ipType) { continue } - if (family !== 4) { - // ignore ipv6 - // eslint-disable-next-line no-continue + if (this.mappedPorts.has(port)) { continue } - const publicIp = this.externalAddress ?? await this.client.externalIp() - const isPrivate = isPrivateIp(publicIp) + output.push(ma) + } + + return output + } + + async mapIpAddresses (): Promise { + if (this.externalAddress == null) { + this.log('discovering public address') + } + + const publicIp = await this.externalAddress.getPublicIp({ + signal: AbortSignal.timeout(this.gatewayDetectionTimeout) + }) + + this.externalAddress ?? await raceSignal(this.client.externalIp(), AbortSignal.timeout(this.gatewayDetectionTimeout), { + errorMessage: `Did not discover a "urn:schemas-upnp-org:device:InternetGatewayDevice:1" device on the local network after ${this.gatewayDetectionTimeout}ms - UPnP may not be configured on your router correctly` + }) + + let ipType: 4 | 6 = 4 + + if (isIPv4(publicIp)) { + ipType = 4 + } else if (isIPv6(publicIp)) { + ipType = 6 + } else { + throw new InvalidIPAddressError(`Public address ${publicIp} was not an IPv4 address`) + } + + // filter addresses to get private, non-relay, IP based addresses that we + // haven't mapped yet + const addresses = this.getUnmappedAddresses(this.addressManager.getAddresses(), ipType) + + if (addresses.length === 0) { + this.log('no private, non-relay, unmapped, IP based addresses found') + return + } + + this.log('%s public IP %s', this.externalAddress != null ? 'using configured' : 'discovered', publicIp) + + this.assertNotBehindDoubleNAT(publicIp) - if (isPrivate === true) { - throw new DoubleNATError(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`) + for (const addr of addresses) { + // try to open uPnP ports for each thin waist address + const { family, host, port, transport } = addr.toOptions() + + if (family === 6) { + // only support IPv4 addresses + continue } - if (isPrivate == null) { - throw new InvalidParametersError(`${publicIp} is not an IP address`) + if (this.mappedPorts.has(port)) { + // already mapped this port + continue } - const publicPort = highPort() + const protocol = transport.toUpperCase() - this.log(`opening uPnP connection from ${publicIp}:${publicPort} to ${host}:${port}`) + this.log(`creating mapping of ${host}:${port}`) - await this.client.map({ - publicPort, - localPort: port, - localAddress: this.localAddress, - protocol: transport.toUpperCase() === 'TCP' ? 'TCP' : 'UDP' + const mappedPort = await this.client.map(port, { + localAddress: host, + protocol: protocol === 'TCP' ? 'TCP' : 'UDP' }) - this.components.addressManager.addObservedAddr(fromNodeAddress({ - family: 4, - address: publicIp, - port: publicPort - }, transport)) + this.mappedPorts.set(port, mappedPort) + + const ma = multiaddr(addr.toString().replace(`/ip${family}/${host}/${transport}/${port}`, `/ip${family}/${publicIp}/${transport}/${mappedPort}`)) + + this.log(`created mapping of ${publicIp}:${mappedPort} to ${host}:${port} as %a`, ma) + + if (this.autoConfirmAddress) { + this.addressManager.confirmObservedAddr(ma) + } else { + this.addressManager.addObservedAddr(ma) + } } } /** - * Stops the NAT manager + * Some ISPs have double-NATs, there's not much we can do with them */ - async stop (): Promise { - if (isBrowser || this.client == null) { - return + private assertNotBehindDoubleNAT (publicIp: string): void { + const isPrivate = isPrivateIp(publicIp) + + if (isPrivate === true) { + throw new DoubleNATError(`${publicIp} is private - please set config.nat.externalIp to an externally routable IP or ensure you are not behind a double NAT`) } - try { - await this.client.close() - } catch (err: any) { - this.log.error(err) + if (isPrivate == null) { + throw new InvalidParametersError(`${publicIp} is not an IP address`) } } } diff --git a/packages/upnp-nat/test/index.spec.ts b/packages/upnp-nat/test/index.spec.ts index 1922437f4a..eefb064e02 100644 --- a/packages/upnp-nat/test/index.spec.ts +++ b/packages/upnp-nat/test/index.spec.ts @@ -1,47 +1,47 @@ /* eslint-env mocha */ import { generateKeyPair } from '@libp2p/crypto/keys' -import { stop } from '@libp2p/interface' +import { TypedEventEmitter, start, stop } from '@libp2p/interface' import { defaultLogger } from '@libp2p/logger' import { peerIdFromPrivateKey } from '@libp2p/peer-id' import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { type StubbedInstance, stubInterface } from 'sinon-ts' import { UPnPNAT } from '../src/upnp-nat.js' +import type { UPnPNATInit } from '../src/index.js' import type { NatAPI } from '@achingbrain/nat-port-mapper' -import type { ComponentLogger, NodeInfo, PeerId } from '@libp2p/interface' -import type { AddressManager, TransportManager } from '@libp2p/interface-internal' +import type { ComponentLogger, Libp2pEvents, NodeInfo, PeerId, TypedEventTarget } from '@libp2p/interface' +import type { AddressManager } from '@libp2p/interface-internal' interface StubbedUPnPNATComponents { peerId: PeerId nodeInfo: NodeInfo logger: ComponentLogger - transportManager: StubbedInstance addressManager: StubbedInstance + events: TypedEventTarget } describe('UPnP NAT (TCP)', () => { const teardown: Array<() => Promise> = [] let client: StubbedInstance - async function createNatManager (natManagerOptions = {}): Promise<{ natManager: any, components: StubbedUPnPNATComponents }> { + async function createNatManager (natManagerOptions: UPnPNATInit = {}): Promise<{ natManager: any, components: StubbedUPnPNATComponents }> { const components: StubbedUPnPNATComponents = { peerId: peerIdFromPrivateKey(await generateKeyPair('Ed25519')), nodeInfo: { name: 'test', version: 'test' }, logger: defaultLogger(), addressManager: stubInterface(), - transportManager: stubInterface() + events: new TypedEventEmitter() } + client = stubInterface() + const natManager = new UPnPNAT(components, { keepAlive: true, + client, ...natManagerOptions }) - client = stubInterface() - - natManager.client = client - teardown.push(async () => { await stop(natManager) }) @@ -52,7 +52,13 @@ describe('UPnP NAT (TCP)', () => { } } - afterEach(async () => Promise.all(teardown.map(async t => { await t() }))) + afterEach(async () => { + await Promise.all( + teardown.map(async t => { + await t() + }) + ) + }) it('should map TCP connections to external ports', async () => { const { @@ -62,21 +68,48 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.12/tcp/4002') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.true() - expect(client.map.getCall(0).args[0]).to.include({ - localPort: 4002, + expect(client.map.getCall(0).args[0]).to.equal(4002) + expect(client.map.getCall(0).args[1]).to.include({ protocol: 'TCP' }) expect(components.addressManager.addObservedAddr.called).to.be.true() }) + it('should map TCP connections to external ports and trust them immediately', async () => { + const { + natManager, + components + } = await createNatManager({ + autoConfirmAddress: true + }) + + client.externalIp.resolves('82.3.1.5') + + components.addressManager.getAddresses.returns([ + multiaddr('/ip4/127.0.0.1/tcp/4002'), + multiaddr('/ip4/192.168.1.12/tcp/4002') + ]) + + await start(natManager) + await natManager.mapIpAddresses() + + expect(client.map.called).to.be.true() + expect(client.map.getCall(0).args[0]).to.equal(4002) + expect(client.map.getCall(0).args[1]).to.include({ + protocol: 'TCP' + }) + expect(components.addressManager.confirmObservedAddr.called).to.be.true() + }) + it('should not map TCP connections when double-natted', async () => { const { natManager, @@ -85,11 +118,12 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('192.168.1.1') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip4/127.0.0.1/tcp/4002'), multiaddr('/ip4/192.168.1.12/tcp/4002') ]) + await start(natManager) await expect(natManager.mapIpAddresses()).to.eventually.be.rejected .with.property('name', 'DoubleNATError') @@ -105,10 +139,11 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip6/fe80::9400:67ff:fe19:2a0f/tcp/0') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.false() @@ -123,10 +158,11 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip6/::1/tcp/0') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.false() @@ -141,10 +177,11 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip4/192.168.1.12/udp/4001') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.false() @@ -159,10 +196,11 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip4/127.0.0.1/tcp/4001') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.false() @@ -177,10 +215,11 @@ describe('UPnP NAT (TCP)', () => { client.externalIp.resolves('82.3.1.5') - components.transportManager.getAddrs.returns([ + components.addressManager.getAddresses.returns([ multiaddr('/ip4/127.0.0.1/tcp/4001/sctp/0') ]) + await start(natManager) await natManager.mapIpAddresses() expect(client.map.called).to.be.false() From 7dcabb884c37dfba69e3ce427544ab05209d137b Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 12 Nov 2024 12:47:52 +0000 Subject: [PATCH 2/2] feat: add dns mappings to address manager (#2818) To allow dynamically adding new multiaddrs to the listening-on list allow adding runtime mappings of ip to domain names. This allows things like the auto-tls component to affect the returned list of multiaddrs to include newly minted domain names. --- .../src/address-manager/index.ts | 13 ++ .../index.ts => address-manager.ts} | 139 ++++++++++++++---- packages/libp2p/src/address-manager/README.md | 43 ------ packages/libp2p/src/address-manager/utils.ts | 13 -- packages/libp2p/src/index.ts | 2 +- packages/libp2p/src/libp2p.ts | 4 +- .../test/addresses/address-manager.spec.ts | 120 +++++++++++++-- .../connection-manager/dial-queue.spec.ts | 2 - .../reconnect-queue.spec.ts | 8 +- .../peer-discovery/peer-discovery.spec.ts | 2 - .../test/transports/transport-manager.spec.ts | 4 +- 11 files changed, 240 insertions(+), 110 deletions(-) rename packages/libp2p/src/{address-manager/index.ts => address-manager.ts} (65%) delete mode 100644 packages/libp2p/src/address-manager/README.md delete mode 100644 packages/libp2p/src/address-manager/utils.ts 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, {