diff --git a/packages/interface-compliance-tests/src/mocks/connection-manager.ts b/packages/interface-compliance-tests/src/mocks/connection-manager.ts index 5d0b4297d4..0d941fb501 100644 --- a/packages/interface-compliance-tests/src/mocks/connection-manager.ts +++ b/packages/interface-compliance-tests/src/mocks/connection-manager.ts @@ -199,6 +199,10 @@ class MockConnectionManager implements ConnectionManager, Startable { getDialQueue (): PendingDial[] { return [] } + + async isDialable (): Promise { + return true + } } export function mockConnectionManager (components: MockConnectionManagerComponents): ConnectionManager { diff --git a/packages/interface-internal/src/connection-manager/index.ts b/packages/interface-internal/src/connection-manager/index.ts index 434fdd5bda..7754641f5d 100644 --- a/packages/interface-internal/src/connection-manager/index.ts +++ b/packages/interface-internal/src/connection-manager/index.ts @@ -1,4 +1,4 @@ -import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId } from '@libp2p/interface' +import type { AbortOptions, PendingDial, Connection, MultiaddrConnection, PeerId, IsDialableOptions } from '@libp2p/interface' import type { PeerMap } from '@libp2p/peer-collections' import type { Multiaddr } from '@multiformats/multiaddr' @@ -80,4 +80,15 @@ export interface ConnectionManager { * ``` */ getDialQueue(): PendingDial[] + + /** + * Given the current node configuration, returns a promise of `true` or + * `false` if the node would attempt to dial the passed multiaddr. + * + * This means a relevant transport is configured, and the connection gater + * would not block the dial attempt. + * + * This may involve resolving DNS addresses so you should pass an AbortSignal. + */ + isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise } diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 478d0cf4eb..eb1022925b 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -324,6 +324,16 @@ export interface PendingDial { export type Libp2pStatus = 'starting' | 'started' | 'stopping' | 'stopped' +export interface IsDialableOptions extends AbortOptions { + /** + * If the dial attempt would open a protocol, and the multiaddr being dialed + * is a circuit relay address, passing true here would cause the test to fail + * because that protocol would not be allowed to run over a data/time limited + * connection. + */ + runOnTransientConnection?: boolean +} + /** * Libp2p nodes implement this interface. */ @@ -608,12 +618,23 @@ export interface Libp2p extends Startable, Ty unregister(id: string): void /** - * Returns the public key for the passed PeerId. If the PeerId is of the 'RSA' type - * this may mean searching the DHT if the key is not present in the KeyStore. - * A set of user defined services + * Returns the public key for the passed PeerId. If the PeerId is of the 'RSA' + * type this may mean searching the routing if the peer's key is not present + * in the peer store. */ getPublicKey(peer: PeerId, options?: AbortOptions): Promise + /** + * Given the current node configuration, returns a promise of `true` or + * `false` if the node would attempt to dial the passed multiaddr. + * + * This means a relevant transport is configured, and the connection gater + * would not block the dial attempt. + * + * This may involve resolving DNS addresses so you should pass an AbortSignal. + */ + isDialable(multiaddr: Multiaddr | Multiaddr[], options?: IsDialableOptions): Promise + /** * A set of user defined services */ diff --git a/packages/libp2p/package.json b/packages/libp2p/package.json index a90638e87e..20c846267e 100644 --- a/packages/libp2p/package.json +++ b/packages/libp2p/package.json @@ -97,6 +97,7 @@ "@libp2p/utils": "^5.2.8", "@multiformats/dns": "^1.0.5", "@multiformats/multiaddr": "^12.2.1", + "@multiformats/multiaddr-matcher": "^1.2.0", "any-signal": "^4.1.1", "datastore-core": "^9.2.9", "interface-datastore": "^8.2.11", @@ -116,7 +117,6 @@ "@libp2p/tcp": "^9.0.19", "@libp2p/websockets": "^8.0.18", "@multiformats/mafmt": "^12.1.6", - "@multiformats/multiaddr-matcher": "^1.2.0", "aegir": "^42.2.5", "delay": "^6.0.0", "it-all": "^3.0.4", diff --git a/packages/libp2p/src/connection-manager/dial-queue.ts b/packages/libp2p/src/connection-manager/dial-queue.ts index 6b97917d09..b45c384018 100644 --- a/packages/libp2p/src/connection-manager/dial-queue.ts +++ b/packages/libp2p/src/connection-manager/dial-queue.ts @@ -5,6 +5,7 @@ import { defaultAddressSort } from '@libp2p/utils/address-sort' import { Queue, type QueueAddOptions } from '@libp2p/utils/queue' import { type Multiaddr, type Resolver, resolvers, multiaddr } from '@multiformats/multiaddr' import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers' +import { Circuit } from '@multiformats/multiaddr-matcher' import { type ClearableSignal, anySignal } from 'any-signal' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { codes } from '../errors.js' @@ -17,7 +18,7 @@ import { MAX_DIAL_QUEUE_LENGTH } from './constants.js' import { resolveMultiaddrs } from './utils.js' -import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting } from '@libp2p/interface' +import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting, IsDialableOptions } from '@libp2p/interface' import type { TransportManager } from '@libp2p/interface-internal' import type { DNS } from '@multiformats/dns' @@ -456,4 +457,27 @@ export class DialQueue { return sortedGatedAddrs } + + async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise { + if (!Array.isArray(multiaddr)) { + multiaddr = [multiaddr] + } + + try { + const addresses = await this.calculateMultiaddrs(undefined, new Set(multiaddr.map(ma => ma.toString())), options) + + if (options.runOnTransientConnection === false) { + // return true if any resolved multiaddrs are not relay addresses + return addresses.find(addr => { + return !Circuit.matches(addr.multiaddr) + }) != null + } + + return true + } catch (err) { + this.log.trace('error calculating if multiaddr(s) were dialable', err) + } + + return false + } } diff --git a/packages/libp2p/src/connection-manager/index.ts b/packages/libp2p/src/connection-manager/index.ts index f859c70c1c..7ffe7916a0 100644 --- a/packages/libp2p/src/connection-manager/index.ts +++ b/packages/libp2p/src/connection-manager/index.ts @@ -10,7 +10,7 @@ import { AutoDial } from './auto-dial.js' import { ConnectionPruner } from './connection-pruner.js' import { AUTO_DIAL_CONCURRENCY, AUTO_DIAL_MAX_QUEUE_LENGTH, AUTO_DIAL_PRIORITY, DIAL_TIMEOUT, INBOUND_CONNECTION_THRESHOLD, MAX_CONNECTIONS, MAX_DIAL_QUEUE_LENGTH, MAX_INCOMING_PENDING_CONNECTIONS, MAX_PARALLEL_DIALS, MAX_PEER_ADDRS_TO_DIAL, MIN_CONNECTIONS } from './constants.js' import { DialQueue } from './dial-queue.js' -import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting } from '@libp2p/interface' +import type { PendingDial, AddressSorter, Libp2pEvents, AbortOptions, ComponentLogger, Logger, Connection, MultiaddrConnection, ConnectionGater, TypedEventTarget, Metrics, PeerId, Peer, PeerStore, Startable, PendingDialStatus, PeerRouting, IsDialableOptions } from '@libp2p/interface' import type { ConnectionManager, OpenConnectionOptions, TransportManager } from '@libp2p/interface-internal' import type { JobStatus } from '@libp2p/utils/queue' @@ -177,7 +177,6 @@ export class DefaultConnectionManager implements ConnectionManager, Startable { public readonly autoDial: AutoDial public readonly connectionPruner: ConnectionPruner private readonly inboundConnectionRateLimiter: RateLimiter - private readonly peerStore: PeerStore private readonly metrics?: Metrics private readonly events: TypedEventTarget @@ -621,4 +620,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable { } }) } + + async isDialable (multiaddr: Multiaddr | Multiaddr[], options: IsDialableOptions = {}): Promise { + return this.dialQueue.isDialable(multiaddr, options) + } } diff --git a/packages/libp2p/src/libp2p.ts b/packages/libp2p/src/libp2p.ts index 9e3a7bb319..4289bb0bf2 100644 --- a/packages/libp2p/src/libp2p.ts +++ b/packages/libp2p/src/libp2p.ts @@ -23,7 +23,7 @@ import { DefaultUpgrader } from './upgrader.js' import * as pkg from './version.js' import type { Components } from './components.js' import type { Libp2p, Libp2pInit, Libp2pOptions } from './index.js' -import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus } from '@libp2p/interface' +import type { PeerRouting, ContentRouting, Libp2pEvents, PendingDial, ServiceMap, AbortOptions, ComponentLogger, Logger, Connection, NewStreamOptions, Stream, Metrics, PeerId, PeerInfo, PeerStore, Topology, Libp2pStatus, IsDialableOptions } from '@libp2p/interface' import type { StreamHandler, StreamHandlerOptions } from '@libp2p/interface-internal' export class Libp2pNode> extends TypedEventEmitter implements Libp2p { @@ -375,6 +375,10 @@ export class Libp2pNode> extends this.components.registrar.unregister(id) } + async isDialable (multiaddr: Multiaddr, options: IsDialableOptions = {}): Promise { + return this.components.connectionManager.isDialable(multiaddr, options) + } + /** * Called whenever peer discovery services emit `peer` events and adds peers * to the peer store. diff --git a/packages/libp2p/test/core/core.spec.ts b/packages/libp2p/test/core/core.spec.ts index bba4d21df3..0794115afb 100644 --- a/packages/libp2p/test/core/core.spec.ts +++ b/packages/libp2p/test/core/core.spec.ts @@ -1,5 +1,8 @@ /* eslint-env mocha */ +import { circuitRelayTransport } from '@libp2p/circuit-relay-v2' +import { webSockets } from '@libp2p/websockets' +import { multiaddr } from '@multiformats/multiaddr' import { expect } from 'aegir/chai' import { createLibp2p } from '../../src/index.js' import type { Libp2p } from '@libp2p/interface' @@ -7,7 +10,7 @@ import type { Libp2p } from '@libp2p/interface' describe('core', () => { let libp2p: Libp2p - after(async () => { + afterEach(async () => { await libp2p.stop() }) @@ -16,4 +19,49 @@ describe('core', () => { expect(libp2p).to.have.property('status', 'started') }) + + it('should say an address is not dialable if we have no transport for it', async () => { + libp2p = await createLibp2p({ + transports: [ + webSockets() + ] + }) + + const ma = multiaddr('/dns4/example.com/sctp/1234') + + await expect(libp2p.isDialable(ma)).to.eventually.be.false() + }) + + it('should say an address is dialable if a transport is configured', async () => { + libp2p = await createLibp2p({ + transports: [ + webSockets() + ] + }) + + const ma = multiaddr('/dns4/example.com/tls/ws') + + await expect(libp2p.isDialable(ma)).to.eventually.be.true() + }) + + it('should test if a protocol can run over a transient connection', async () => { + libp2p = await createLibp2p({ + transports: [ + webSockets(), + circuitRelayTransport() + ] + }) + + await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws'), { + runOnTransientConnection: false + })).to.eventually.be.true() + + await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), { + runOnTransientConnection: true + })).to.eventually.be.true() + + await expect(libp2p.isDialable(multiaddr('/dns4/example.com/tls/ws/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE1/p2p-circuit/p2p/12D3KooWSExt8hTzoaHEhn435BTK6BPNSY1LpTc1j2o9Gw53tXE2'), { + runOnTransientConnection: false + })).to.eventually.be.false() + }) })