Skip to content

Commit

Permalink
feat: support custom DNS resolvers (#2435)
Browse files Browse the repository at this point in the history
Adds a `dns` config key that lets users specify an instance of the
`DNS` interface from `@multiformats/dns`.

This instance will be passed through to DNSADDR `multiaddr`s for use
when the DNSLink TXT record is resolved.
  • Loading branch information
achingbrain authored Mar 12, 2024
1 parent 25c8f93 commit f39ce5f
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 119 deletions.
3 changes: 2 additions & 1 deletion packages/libp2p/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"@libp2p/peer-id-factory": "^4.0.7",
"@libp2p/peer-store": "^10.0.11",
"@libp2p/utils": "^5.2.6",
"@multiformats/multiaddr": "^12.1.14",
"@multiformats/dns": "^1.0.1",
"@multiformats/multiaddr": "^12.2.0",
"any-signal": "^4.1.1",
"datastore-core": "^9.2.8",
"interface-datastore": "^8.2.11",
Expand Down
6 changes: 5 additions & 1 deletion packages/libp2p/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CodeError } from '@libp2p/interface'
import { isStartable, type Startable, type Libp2pEvents, type ComponentLogger, type NodeInfo, type ConnectionProtector, type ConnectionGater, type ContentRouting, type TypedEventTarget, type Metrics, type PeerId, type PeerRouting, type PeerStore, type PrivateKey, type Upgrader } from '@libp2p/interface'
import { defaultLogger } from '@libp2p/logger'
import type { AddressManager, ConnectionManager, Registrar, TransportManager } from '@libp2p/interface-internal'
import type { DNS } from '@multiformats/dns'
import type { Datastore } from 'interface-datastore'

export interface Components extends Record<string, any>, Startable {
Expand All @@ -22,6 +23,7 @@ export interface Components extends Record<string, any>, Startable {
datastore: Datastore
connectionProtector?: ConnectionProtector
metrics?: Metrics
dns?: DNS
}

export interface ComponentsInit {
Expand All @@ -42,6 +44,7 @@ export interface ComponentsInit {
peerRouting?: PeerRouting
datastore?: Datastore
connectionProtector?: ConnectionProtector
dns?: DNS
}

class DefaultComponents implements Startable {
Expand Down Expand Up @@ -103,7 +106,8 @@ class DefaultComponents implements Startable {

const OPTIONAL_SERVICES = [
'metrics',
'connectionProtector'
'connectionProtector',
'dns'
]

const NON_SERVICE_PROPERTIES = [
Expand Down
3 changes: 3 additions & 0 deletions packages/libp2p/src/connection-manager/dial-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { resolveMultiaddrs } from './utils.js'
import type { AddressSorter, AbortOptions, ComponentLogger, Logger, Connection, ConnectionGater, Metrics, PeerId, Address, PeerStore, PeerRouting } from '@libp2p/interface'
import type { TransportManager } from '@libp2p/interface-internal'
import type { DNS } from '@multiformats/dns'

export interface PendingDialTarget {
resolve(value: any): void
Expand Down Expand Up @@ -61,6 +62,7 @@ interface DialQueueComponents {
transportManager: TransportManager
connectionGater: ConnectionGater
logger: ComponentLogger
dns?: DNS
}

export class DialQueue {
Expand Down Expand Up @@ -344,6 +346,7 @@ export class DialQueue {
let resolvedAddresses = (await Promise.all(
addrs.map(async addr => {
const result = await resolveMultiaddrs(addr.multiaddr, {
dns: this.components.dns,
...options,
log: this.log
})
Expand Down
51 changes: 17 additions & 34 deletions packages/libp2p/src/connection-manager/utils.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
import { type AbortOptions, multiaddr, type Multiaddr } from '@multiformats/multiaddr'
import { resolvers } from '@multiformats/multiaddr'
import type { LoggerOptions } from '@libp2p/interface'
import type { Multiaddr, ResolveOptions } from '@multiformats/multiaddr'

/**
* Resolve multiaddr recursively
* Recursively resolve DNSADDR multiaddrs
*/
export async function resolveMultiaddrs (ma: Multiaddr, options: AbortOptions & LoggerOptions): Promise<Multiaddr[]> {
// TODO: recursive logic should live in multiaddr once dns4/dns6 support is in place
// Now only supporting resolve for dnsaddr
const resolvableProto = ma.protoNames().includes('dnsaddr')
export async function resolveMultiaddrs (ma: Multiaddr, options: ResolveOptions & LoggerOptions): Promise<Multiaddr[]> {
// check multiaddr resolvers
let resolvable = false

// Multiaddr is not resolvable? End recursion!
if (!resolvableProto) {
return [ma]
for (const key of resolvers.keys()) {
resolvable = ma.protoNames().includes(key)

if (resolvable) {
break
}
}

const resolvedMultiaddrs = await resolveRecord(ma, options)
const recursiveMultiaddrs = await Promise.all(resolvedMultiaddrs.map(async (nm) => {
return resolveMultiaddrs(nm, options)
}))
// return multiaddr if it is not resolvable
if (!resolvable) {
return [ma]
}

const addrs = recursiveMultiaddrs.flat()
const output = addrs.reduce<Multiaddr[]>((array, newM) => {
if (array.find(m => m.equals(newM)) == null) {
array.push(newM)
}
return array
}, ([]))
const output = await ma.resolve(options)

options.log('resolved %s to', ma, output.map(ma => ma.toString()))

return output
}

/**
* Resolve a given multiaddr. If this fails, an empty array will be returned
*/
async function resolveRecord (ma: Multiaddr, options: AbortOptions & LoggerOptions): Promise<Multiaddr[]> {
try {
ma = multiaddr(ma.toString()) // Use current multiaddr module
const multiaddrs = await ma.resolve(options)
return multiaddrs
} catch (err) {
options.log.error(`multiaddr ${ma.toString()} could not be resolved`, err)
return []
}
}
8 changes: 8 additions & 0 deletions packages/libp2p/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { ConnectionManagerInit } from './connection-manager/index.js'
import type { TransportManagerInit } from './transport-manager.js'
import type { Libp2p, ServiceMap, RecursivePartial, ComponentLogger, NodeInfo, ConnectionProtector, ConnectionEncrypter, ConnectionGater, ContentRouting, Metrics, PeerDiscovery, PeerId, PeerRouting, StreamMuxerFactory, Transport, PrivateKey } from '@libp2p/interface'
import type { PersistentPeerStoreInit } from '@libp2p/peer-store'
import type { DNS } from '@multiformats/dns'
import type { Datastore } from 'interface-datastore'

export type ServiceFactoryMap<T extends Record<string, unknown> = Record<string, unknown>> = {
Expand Down Expand Up @@ -123,6 +124,13 @@ export interface Libp2pInit<T extends ServiceMap = { x: Record<string, unknown>
* ```
*/
logger?: ComponentLogger

/**
* An optional DNS resolver configuration. If omitted the default DNS resolver
* for the platform will be used which means `node:dns` on Node.js and
* DNS-JSON-over-HTTPS for browsers using Google and Cloudflare servers.
*/
dns?: DNS
}

export type { Libp2p }
Expand Down
3 changes: 2 additions & 1 deletion packages/libp2p/src/libp2p.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ export class Libp2pNode<T extends ServiceMap = Record<string, unknown>> extends
logger: this.logger,
events,
datastore: init.datastore ?? new MemoryDatastore(),
connectionGater: connectionGater(init.connectionGater)
connectionGater: connectionGater(init.connectionGater),
dns: init.dns
})

this.peerStore = this.configureComponent('peerStore', new PersistentPeerStore(components, {
Expand Down
26 changes: 26 additions & 0 deletions packages/libp2p/test/connection-manager/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { TypedEventEmitter, start } from '@libp2p/interface'
import { mockConnection, mockDuplex, mockMultiaddrConnection } from '@libp2p/interface-compliance-tests/mocks'
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { dns } from '@multiformats/dns'
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import delay from 'delay'
import all from 'it-all'
Expand Down Expand Up @@ -390,6 +392,30 @@ describe('libp2p.connections', () => {
})
expect(libp2p.components.connectionManager.getConnections()).to.have.lengthOf(2)
})

it('should use custom DNS resolver', async () => {
const resolver = sinon.stub()

libp2p = await createNode({
config: createBaseOptions({
addresses: {
listen: ['/ip4/127.0.0.1/tcp/0/ws']
},
dns: dns({
resolvers: {
'.': resolver
}
})
})
})

const ma = multiaddr('/dnsaddr/example.com/tcp/12345')
const err = new Error('Could not resolve')

resolver.withArgs('_dnsaddr.example.com').rejects(err)

await expect(libp2p.dial(ma)).to.eventually.be.rejectedWith(err)
})
})

describe('connection gater', () => {
Expand Down
85 changes: 3 additions & 82 deletions packages/libp2p/test/connection-manager/resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,12 @@ import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
import pDefer from 'p-defer'
import sinon from 'sinon'
import { codes as ErrorCodes } from '../../src/errors.js'
import { createLibp2pNode, type Libp2pNode } from '../../src/libp2p.js'
import type { PeerId, Transport } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'

const relayAddr = multiaddr(process.env.RELAY_MULTIADDR)

const getDnsaddrStub = (peerId: PeerId): string[] => [
`/dnsaddr/ams-1.bootstrap.libp2p.io/p2p/${peerId.toString()}`,
`/dnsaddr/ams-2.bootstrap.libp2p.io/p2p/${peerId.toString()}`,
`/dnsaddr/lon-1.bootstrap.libp2p.io/p2p/${peerId.toString()}`,
`/dnsaddr/nrt-1.bootstrap.libp2p.io/p2p/${peerId.toString()}`,
`/dnsaddr/nyc-1.bootstrap.libp2p.io/p2p/${peerId.toString()}`,
`/dnsaddr/sfo-2.bootstrap.libp2p.io/p2p/${peerId.toString()}`
]

const relayedAddr = (peerId: PeerId): string => `${relayAddr.toString()}/p2p-circuit/p2p/${peerId.toString()}`

const getDnsRelayedAddrStub = (peerId: PeerId): string[] => [
Expand Down Expand Up @@ -140,41 +130,6 @@ describe('dialing (resolvable addresses)', () => {
expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true)
})

it('resolves a dnsaddr recursively', async () => {
const remoteId = remoteLibp2p.peerId
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`)
const relayedAddrFetched = multiaddr(relayedAddr(remoteId))

const relayId = await createEd25519PeerId()
// ensure remote libp2p creates reservation on relay
await remoteLibp2p.peerStore.merge(relayId, {
protocols: [RELAY_V2_HOP_CODEC]
})

// Transport spy
const transport = getTransport(libp2p, 'libp2p/circuit-relay-v2')
const transportDialSpy = sinon.spy(transport, 'dial')

// Resolver stub
let firstCall = false
resolver.callsFake(async () => {
if (!firstCall) {
firstCall = true
// Return an array of dnsaddr
return Promise.resolve(getDnsaddrStub(remoteId))
}
return Promise.resolve(getDnsRelayedAddrStub(remoteId))
})

// Dial with address resolve
const connection = await libp2p.dial(dialAddr)
expect(connection).to.exist()
expect(connection.remoteAddr.equals(relayedAddrFetched))

const dialArgs = transportDialSpy.firstCall.args
expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true)
})

// TODO: Temporary solution does not resolve dns4/dns6
// Resolver just returns the received multiaddrs
it('stops recursive resolve if finds dns4/dns6 and dials it', async () => {
Expand Down Expand Up @@ -205,50 +160,16 @@ describe('dialing (resolvable addresses)', () => {
await deferred.promise
})

it('resolves a dnsaddr recursively not failing if one address fails to resolve', async () => {
const remoteId = remoteLibp2p.peerId
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`)
const relayedAddrFetched = multiaddr(relayedAddr(remoteId))

const relayId = await createEd25519PeerId()
// ensure remote libp2p creates reservation on relay
await remoteLibp2p.peerStore.merge(relayId, {
protocols: [RELAY_V2_HOP_CODEC]
})

// Transport spy
const transport = getTransport(libp2p, 'libp2p/circuit-relay-v2')
const transportDialSpy = sinon.spy(transport, 'dial')

// Resolver stub
resolver.onCall(0).callsFake(async () => Promise.resolve(getDnsaddrStub(remoteId)))
resolver.onCall(1).callsFake(async () => Promise.reject(new Error()))
resolver.callsFake(async () => Promise.resolve(getDnsRelayedAddrStub(remoteId)))

// Dial with address resolve
const connection = await libp2p.dial(dialAddr)
expect(connection).to.exist()
expect(connection.remoteAddr.equals(relayedAddrFetched))

const dialArgs = transportDialSpy.firstCall.args
expect(dialArgs[0].equals(relayedAddrFetched)).to.eql(true)
})

it('fails to dial if resolve fails and there are no addresses to dial', async () => {
const remoteId = remoteLibp2p.peerId
const dialAddr = multiaddr(`/dnsaddr/remote.libp2p.io/p2p/${remoteId.toString()}`)
const err = new Error()

// Stub resolver
resolver.returns(Promise.reject(new Error()))

// Stub transport
const transport = getTransport(libp2p, '@libp2p/websockets')
const spy = sinon.spy(transport, 'dial')
resolver.returns(Promise.reject(err))

await expect(libp2p.dial(dialAddr))
.to.eventually.be.rejectedWith(Error)
.and.to.have.nested.property('.code', ErrorCodes.ERR_NO_VALID_ADDRESSES)
expect(spy.callCount).to.eql(0)
.to.eventually.be.rejectedWith(err)
})
})

Expand Down

0 comments on commit f39ce5f

Please sign in to comment.