Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom DNS resolvers #2435

Merged
merged 4 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
achingbrain marked this conversation as resolved.
Show resolved Hide resolved
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
Loading