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: accept DNS resolver when resolving DNSADDR addresses #373

Merged
merged 3 commits into from
Mar 11, 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
53 changes: 41 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,54 @@ 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 |
See the docs for [@multiformats/dns](https://www.npmjs.com/package/@multiformats/dns) for a full breakdown of how to specify multiple resolvers or resolvers that can be used for specific TLDs.

A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
```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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth calling out what the dot here is for, or adding example with more specific tld resolver

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've linked to the @multiformats/dns docs to avoid replicating them here.

})

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
80 changes: 61 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,60 @@
* // 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
*
* See the docs for [@multiformats/dns](https://www.npmjs.com/package/@multiformats/dns) for a full breakdown of how to specify multiple resolvers or resolvers that can be used for specific TLDs.
*
* ```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')
* })
*
* | Name | type | Description |
* | ----------------- | --------- | ----------------------------------- |
* | `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records |
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
* const resolved = await ma.resolve({
* dns: resolver
* })
*
* A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
* 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 +133,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 @@ -130,8 +155,25 @@ export interface AbortOptions {
*/
export const resolvers = new Map<string, Resolver>()

export type { 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 +430,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
10 changes: 6 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, multiaddr, 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,9 @@ 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))
const result = await resolver(this, options)

return result.map(str => multiaddr(str))
}

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 dnsaddrResolver: Resolver<DNSADDROptions> = async function dnsaddr (ma: Multiaddr, options: DNSADDROptions = {}): Promise<string[]> {
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: string[] = []

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.map(ma => ma.toString()))
} else {
output.push(ma.toString())
}
}

return output
}
Loading
Loading