Skip to content

Commit

Permalink
feat: accept DNS resolver when resolving DNSADDR addresses
Browse files Browse the repository at this point in the history
Accept a `DNS` instance from `@multiformats/dns` when resolving
DNSADDR addresses.

Gives the user flexibility to control which DNS servers are used
to resolve TXT records.
  • Loading branch information
achingbrain committed Mar 8, 2024
1 parent 9ce73b2 commit 5be4646
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 182 deletions.
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A standard way to represent addresses that

## Example

```js
```TypeScript
import { multiaddr } from '@multiformats/multiaddr'
const addr = multiaddr("/ip4/127.0.0.1/udp/1234")
// Multiaddr(/ip4/127.0.0.1/udp/1234)
Expand Down Expand Up @@ -65,25 +65,52 @@ addr.encapsulate('/sctp/5678')
// Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678)
```

## Resolvers
## Resolving DNSADDR addresses

`multiaddr` allows multiaddrs to be resolved when appropriate resolvers are provided. This module already has resolvers available, but you can also create your own. Resolvers should always be set in the same module that is calling `multiaddr.resolve()` to avoid conflicts if multiple versions of `multiaddr` are in your dependency tree.
[DNSADDR](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) is a spec that allows storing a TXT DNS record that contains a Multiaddr.

To provide multiaddr resolvers you can do:
To resolve DNSADDR addresses, call the `.resolve()` function the multiaddr, optionally passing a `DNS` resolver.

```js
import { resolvers } from '@multiformats/multiaddr'
DNSADDR addresses can resolve to multiple multiaddrs, since there is no limit to the number of TXT records that can be stored.

resolvers.set('dnsaddr', resolvers.dnsaddrResolver)
## Example - Resolving DNSADDR Multiaddrs

```TypeScript
import { multiaddr, resolvers } from '@multiformats/multiaddr'
import { dnsaddr } from '@multiformats/multiaddr/resolvers'

resolvers.set('dnsaddr', dnsaddr)

const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')

// resolve with a 5s timeout
const resolved = await ma.resolve({
signal: AbortSignal.timeout(5000)
})

console.info(await ma.resolve(resolved)
// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
```
The available resolvers are:
## Example - Using a custom DNS resolver to resolve DNSADDR Multiaddrs
| Name | type | Description |
| ----------------- | --------- | ----------------------------------- |
| `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records |
```TypeScript
import { multiaddr } from '@multiformats/multiaddr'
import { dns } from '@multiformats/dns'
import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'

const resolver = dns({
'.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query')
})

A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
const resolved = await ma.resolve({
dns: resolver
})

console.info(resolved)
// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
```
# Install
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,17 @@
"@chainsafe/is-ip": "^2.0.1",
"@chainsafe/netmask": "^2.0.0",
"@libp2p/interface": "^1.0.0",
"dns-over-http-resolver": "^3.0.2",
"@multiformats/dns": "^1.0.1",
"multiformats": "^13.0.0",
"race-signal": "^1.0.2",
"uint8-varint": "^2.0.1",
"uint8arrays": "^5.0.0"
},
"devDependencies": {
"@types/sinon": "^17.0.2",
"aegir": "^42.2.2",
"sinon": "^17.0.0"
"sinon": "^17.0.0",
"sinon-ts": "^2.0.0"
},
"browser": {
"./dist/src/resolvers/dns.js": "./dist/src/resolvers/dns.browser.js"
Expand Down
76 changes: 57 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*
* @example
*
* ```js
* ```TypeScript
* import { multiaddr } from '@multiformats/multiaddr'
* const addr = multiaddr("/ip4/127.0.0.1/udp/1234")
* // Multiaddr(/ip4/127.0.0.1/udp/1234)
Expand Down Expand Up @@ -43,29 +43,58 @@
* // Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678)
* ```
*
* ## Resolvers
* ## Resolving DNSADDR addresses
*
* `multiaddr` allows multiaddrs to be resolved when appropriate resolvers are provided. This module already has resolvers available, but you can also create your own. Resolvers should always be set in the same module that is calling `multiaddr.resolve()` to avoid conflicts if multiple versions of `multiaddr` are in your dependency tree.
* [DNSADDR](https://github.com/multiformats/multiaddr/blob/master/protocols/DNSADDR.md) is a spec that allows storing a TXT DNS record that contains a Multiaddr.
*
* To provide multiaddr resolvers you can do:
* To resolve DNSADDR addresses, call the `.resolve()` function the multiaddr, optionally passing a `DNS` resolver.
*
* ```js
* import { resolvers } from '@multiformats/multiaddr'
* DNSADDR addresses can resolve to multiple multiaddrs, since there is no limit to the number of TXT records that can be stored.
*
* @example Resolving DNSADDR Multiaddrs
*
* ```TypeScript
* import { multiaddr, resolvers } from '@multiformats/multiaddr'
* import { dnsaddr } from '@multiformats/multiaddr/resolvers'
*
* resolvers.set('dnsaddr', dnsaddr)
*
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
*
* resolvers.set('dnsaddr', resolvers.dnsaddrResolver)
* // resolve with a 5s timeout
* const resolved = await ma.resolve({
* signal: AbortSignal.timeout(5000)
* })
*
* console.info(await ma.resolve(resolved)
* // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
* ```
*
* The available resolvers are:
* @example Using a custom DNS resolver to resolve DNSADDR Multiaddrs
*
* ```TypeScript
* import { multiaddr } from '@multiformats/multiaddr'
* import { dns } from '@multiformats/dns'
* import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'
*
* | Name | type | Description |
* | ----------------- | --------- | ----------------------------------- |
* | `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records |
* const resolver = dns({
* '.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query')
* })
*
* A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
* const resolved = await ma.resolve({
* dns: resolver
* })
*
* console.info(resolved)
* // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
* ```
*/

import { Multiaddr as MultiaddrClass, symbol } from './multiaddr.js'
import { getProtocol } from './protocols-table.js'
import type { Resolver } from './resolvers/index.js'
import type { DNS } from '@multiformats/dns'

/**
* Protocols are present in the protocol table
Expand Down Expand Up @@ -102,12 +131,6 @@ export interface NodeAddress {
*/
export type MultiaddrInput = string | Multiaddr | Uint8Array | null

/**
* A Resolver is a function that takes a {@link Multiaddr} and resolves it into one
* or more string representations of that {@link Multiaddr}.
*/
export interface Resolver { (addr: Multiaddr, options?: AbortOptions): Promise<string[]> }

/**
* A code/value pair
*/
Expand All @@ -132,6 +155,21 @@ export const resolvers = new Map<string, Resolver>()

export { MultiaddrFilter } from './filter/multiaddr-filter.js'

export interface ResolveOptions extends AbortOptions {
/**
* An optional DNS resolver
*/
dns?: DNS

/**
* When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs,
* limit how many times we will recursively resolve them.
*
* @default 32
*/
maxRecursiveDepth?: number
}

export interface Multiaddr {
bytes: Uint8Array

Expand Down Expand Up @@ -388,7 +426,7 @@ export interface Multiaddr {
* // ]
* ```
*/
resolve(options?: AbortOptions): Promise<Multiaddr[]>
resolve(options?: ResolveOptions): Promise<Multiaddr[]>

/**
* Gets a Multiaddrs node-friendly address object. Note that protocol information
Expand Down
8 changes: 4 additions & 4 deletions src/multiaddr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import { bytesToMultiaddrParts, stringToMultiaddrParts, type MultiaddrParts, tuplesToBytes } from './codec.js'
import { getProtocol, names } from './protocols-table.js'
import { isMultiaddr, type AbortOptions, type MultiaddrInput, type Multiaddr as MultiaddrInterface, type MultiaddrObject, type Protocol, type StringTuple, type Tuple, resolvers, type NodeAddress } from './index.js'
import { isMultiaddr, resolvers } from './index.js'
import type { MultiaddrInput, Multiaddr as MultiaddrInterface, MultiaddrObject, Protocol, StringTuple, Tuple, NodeAddress, ResolveOptions } from './index.js'

const inspect = Symbol.for('nodejs.util.inspect.custom')
export const symbol = Symbol.for('@multiformats/js-multiaddr/multiaddr')
Expand Down Expand Up @@ -221,7 +222,7 @@ export class Multiaddr implements MultiaddrInterface {
return uint8ArrayEquals(this.bytes, addr.bytes)
}

async resolve (options?: AbortOptions): Promise<Multiaddr[]> {
async resolve (options?: ResolveOptions): Promise<MultiaddrInterface[]> {
const resolvableProto = this.protos().find((p) => p.resolvable)

// Multiaddr is not resolvable?
Expand All @@ -234,8 +235,7 @@ export class Multiaddr implements MultiaddrInterface {
throw new CodeError(`no available resolver for ${resolvableProto.name}`, 'ERR_NO_AVAILABLE_RESOLVER')
}

const addresses = await resolver(this, options)
return addresses.map((a) => new Multiaddr(a))
return resolver(this, options)
}

nodeAddress (): NodeAddress {
Expand Down
3 changes: 0 additions & 3 deletions src/resolvers/dns.browser.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/resolvers/dns.ts

This file was deleted.

74 changes: 74 additions & 0 deletions src/resolvers/dnsaddr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { CodeError } from '@libp2p/interface'
import { dns, RecordType } from '@multiformats/dns'
import { raceSignal } from 'race-signal'
import { multiaddr } from '../index.js'
import { getProtocol } from '../protocols-table.js'
import type { Resolver } from './index.js'
import type { AbortOptions, Multiaddr } from '../index.js'
import type { DNS } from '@multiformats/dns'

const MAX_RECURSIVE_DEPTH = 32
const { code: dnsaddrCode } = getProtocol('dnsaddr')

export interface DNSADDROptions extends AbortOptions {
/**
* An optional DNS resolver
*/
dns?: DNS

/**
* When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs,
* limit how many times we will recursively resolve them.
*
* @default 32
*/
maxRecursiveDepth?: number
}

export const dnsaddr: Resolver<DNSADDROptions> = async function dnsaddr (ma: Multiaddr, options: DNSADDROptions = {}): Promise<Multiaddr[]> {
const recursionLimit = options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH

if (recursionLimit === 0) {
throw new CodeError('Max recursive depth reached', 'ERR_MAX_RECURSIVE_DEPTH_REACHED')
}

const [, hostname] = ma.stringTuples().find(([proto]) => proto === dnsaddrCode) ?? []

const resolver = options?.dns ?? dns()
const result = await raceSignal(resolver.query(`_dnsaddr.${hostname}`, {
signal: options?.signal,
types: [
RecordType.TXT
]
}), options.signal)

const peerId = ma.getPeerId()
const output: Multiaddr[] = []

for (const answer of result.Answer) {
const addr = answer.data.split('=')[1]

if (addr == null) {
continue
}

if (peerId != null && !addr.includes(peerId)) {
continue
}

const ma = multiaddr(addr)

if (addr.startsWith('/dnsaddr')) {
const resolved = await ma.resolve({
...options,
maxRecursiveDepth: recursionLimit - 1
})

output.push(...resolved)
} else {
output.push(ma)
}
}

return output
}
60 changes: 4 additions & 56 deletions src/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,7 @@
/**
* @packageDocumentation
*
* Provides strategies for resolving multiaddrs.
*/

import { getProtocol } from '../protocols-table.js'
import Resolver from './dns.js'
import type { AbortOptions, Multiaddr } from '../index.js'

const { code: dnsaddrCode } = getProtocol('dnsaddr')

/**
* Resolver for dnsaddr addresses.
*
* @example
*
* ```typescript
* import { dnsaddrResolver } from '@multiformats/multiaddr/resolvers'
* import { multiaddr } from '@multiformats/multiaddr'
*
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
* const addresses = await dnsaddrResolver(ma)
*
* console.info(addresses)
* //[
* // '/dnsaddr/am6.bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb',
* // '/dnsaddr/ny5.bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa',
* // '/dnsaddr/sg1.bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt',
* // '/dnsaddr/sv15.bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN'
* //]
* ```
*/
export async function dnsaddrResolver (addr: Multiaddr, options: AbortOptions = {}): Promise<string[]> {
const resolver = new Resolver()

if (options.signal != null) {
options.signal.addEventListener('abort', () => {
resolver.cancel()
})
}

const peerId = addr.getPeerId()
const [, hostname] = addr.stringTuples().find(([proto]) => proto === dnsaddrCode) ?? []

if (hostname == null) {
throw new Error('No hostname found in multiaddr')
}

const records = await resolver.resolveTxt(`_dnsaddr.${hostname}`)

let addresses = records.flat().map((a) => a.split('=')[1]).filter(Boolean)

if (peerId != null) {
addresses = addresses.filter((entry) => entry.includes(peerId))
}

return addresses
export interface Resolver<ResolveOptions extends AbortOptions = AbortOptions> {
(ma: Multiaddr, options?: ResolveOptions): Promise<Multiaddr[]>
}

export { dnsaddr } from './dnsaddr.js'
Loading

0 comments on commit 5be4646

Please sign in to comment.