Skip to content

Commit 9b2df99

Browse files
authored
feat: accept DNS resolver when resolving DNSADDR addresses (#373)
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.
1 parent 9ce73b2 commit 9b2df99

File tree

9 files changed

+283
-182
lines changed

9 files changed

+283
-182
lines changed

README.md

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ A standard way to represent addresses that
3333

3434
## Example
3535

36-
```js
36+
```TypeScript
3737
import { multiaddr } from '@multiformats/multiaddr'
3838
const addr = multiaddr("/ip4/127.0.0.1/udp/1234")
3939
// Multiaddr(/ip4/127.0.0.1/udp/1234)
@@ -65,25 +65,54 @@ addr.encapsulate('/sctp/5678')
6565
// Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678)
6666
```
6767

68-
## Resolvers
68+
## Resolving DNSADDR addresses
6969

70-
`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.
70+
[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.
7171

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

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

77-
resolvers.set('dnsaddr', resolvers.dnsaddrResolver)
76+
## Example - Resolving DNSADDR Multiaddrs
77+
78+
```TypeScript
79+
import { multiaddr, resolvers } from '@multiformats/multiaddr'
80+
import { dnsaddr } from '@multiformats/multiaddr/resolvers'
81+
82+
resolvers.set('dnsaddr', dnsaddr)
83+
84+
const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
85+
86+
// resolve with a 5s timeout
87+
const resolved = await ma.resolve({
88+
signal: AbortSignal.timeout(5000)
89+
})
90+
91+
console.info(await ma.resolve(resolved)
92+
// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
7893
```
7994
80-
The available resolvers are:
95+
## Example - Using a custom DNS resolver to resolve DNSADDR Multiaddrs
8196
82-
| Name | type | Description |
83-
| ----------------- | --------- | ----------------------------------- |
84-
| `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records |
97+
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.
8598
86-
A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
99+
```TypeScript
100+
import { multiaddr } from '@multiformats/multiaddr'
101+
import { dns } from '@multiformats/dns'
102+
import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'
103+
104+
const resolver = dns({
105+
'.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query')
106+
})
107+
108+
const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
109+
const resolved = await ma.resolve({
110+
dns: resolver
111+
})
112+
113+
console.info(resolved)
114+
// [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
115+
```
87116
88117
# Install
89118

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,17 @@
169169
"@chainsafe/is-ip": "^2.0.1",
170170
"@chainsafe/netmask": "^2.0.0",
171171
"@libp2p/interface": "^1.0.0",
172-
"dns-over-http-resolver": "^3.0.2",
172+
"@multiformats/dns": "^1.0.1",
173173
"multiformats": "^13.0.0",
174+
"race-signal": "^1.0.2",
174175
"uint8-varint": "^2.0.1",
175176
"uint8arrays": "^5.0.0"
176177
},
177178
"devDependencies": {
178179
"@types/sinon": "^17.0.2",
179180
"aegir": "^42.2.2",
180-
"sinon": "^17.0.0"
181+
"sinon": "^17.0.0",
182+
"sinon-ts": "^2.0.0"
181183
},
182184
"browser": {
183185
"./dist/src/resolvers/dns.js": "./dist/src/resolvers/dns.browser.js"

src/index.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*
1212
* @example
1313
*
14-
* ```js
14+
* ```TypeScript
1515
* import { multiaddr } from '@multiformats/multiaddr'
1616
* const addr = multiaddr("/ip4/127.0.0.1/udp/1234")
1717
* // Multiaddr(/ip4/127.0.0.1/udp/1234)
@@ -43,29 +43,60 @@
4343
* // Multiaddr(/ip4/127.0.0.1/udp/1234/sctp/5678)
4444
* ```
4545
*
46-
* ## Resolvers
46+
* ## Resolving DNSADDR addresses
4747
*
48-
* `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.
48+
* [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.
4949
*
50-
* To provide multiaddr resolvers you can do:
50+
* To resolve DNSADDR addresses, call the `.resolve()` function the multiaddr, optionally passing a `DNS` resolver.
5151
*
52-
* ```js
53-
* import { resolvers } from '@multiformats/multiaddr'
52+
* DNSADDR addresses can resolve to multiple multiaddrs, since there is no limit to the number of TXT records that can be stored.
53+
*
54+
* @example Resolving DNSADDR Multiaddrs
55+
*
56+
* ```TypeScript
57+
* import { multiaddr, resolvers } from '@multiformats/multiaddr'
58+
* import { dnsaddr } from '@multiformats/multiaddr/resolvers'
59+
*
60+
* resolvers.set('dnsaddr', dnsaddr)
61+
*
62+
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
5463
*
55-
* resolvers.set('dnsaddr', resolvers.dnsaddrResolver)
64+
* // resolve with a 5s timeout
65+
* const resolved = await ma.resolve({
66+
* signal: AbortSignal.timeout(5000)
67+
* })
68+
*
69+
* console.info(await ma.resolve(resolved)
70+
* // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
5671
* ```
5772
*
58-
* The available resolvers are:
73+
* @example Using a custom DNS resolver to resolve DNSADDR Multiaddrs
74+
*
75+
* 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.
76+
*
77+
* ```TypeScript
78+
* import { multiaddr } from '@multiformats/multiaddr'
79+
* import { dns } from '@multiformats/dns'
80+
* import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'
81+
*
82+
* const resolver = dns({
83+
* '.': dnsJsonOverHttps('https://cloudflare-dns.com/dns-query')
84+
* })
5985
*
60-
* | Name | type | Description |
61-
* | ----------------- | --------- | ----------------------------------- |
62-
* | `dnsaddrResolver` | `dnsaddr` | dnsaddr resolution with TXT Records |
86+
* const ma = multiaddr('/dnsaddr/bootstrap.libp2p.io')
87+
* const resolved = await ma.resolve({
88+
* dns: resolver
89+
* })
6390
*
64-
* A resolver receives a `Multiaddr` as a parameter and returns a `Promise<Array<string>>`.
91+
* console.info(resolved)
92+
* // [Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...'), Multiaddr('/ip4/147.75...')...]
93+
* ```
6594
*/
6695

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

70101
/**
71102
* Protocols are present in the protocol table
@@ -102,12 +133,6 @@ export interface NodeAddress {
102133
*/
103134
export type MultiaddrInput = string | Multiaddr | Uint8Array | null
104135

105-
/**
106-
* A Resolver is a function that takes a {@link Multiaddr} and resolves it into one
107-
* or more string representations of that {@link Multiaddr}.
108-
*/
109-
export interface Resolver { (addr: Multiaddr, options?: AbortOptions): Promise<string[]> }
110-
111136
/**
112137
* A code/value pair
113138
*/
@@ -130,8 +155,25 @@ export interface AbortOptions {
130155
*/
131156
export const resolvers = new Map<string, Resolver>()
132157

158+
export type { Resolver }
159+
133160
export { MultiaddrFilter } from './filter/multiaddr-filter.js'
134161

162+
export interface ResolveOptions extends AbortOptions {
163+
/**
164+
* An optional DNS resolver
165+
*/
166+
dns?: DNS
167+
168+
/**
169+
* When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs,
170+
* limit how many times we will recursively resolve them.
171+
*
172+
* @default 32
173+
*/
174+
maxRecursiveDepth?: number
175+
}
176+
135177
export interface Multiaddr {
136178
bytes: Uint8Array
137179

@@ -388,7 +430,7 @@ export interface Multiaddr {
388430
* // ]
389431
* ```
390432
*/
391-
resolve(options?: AbortOptions): Promise<Multiaddr[]>
433+
resolve(options?: ResolveOptions): Promise<Multiaddr[]>
392434

393435
/**
394436
* Gets a Multiaddrs node-friendly address object. Note that protocol information

src/multiaddr.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
1919
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
2020
import { bytesToMultiaddrParts, stringToMultiaddrParts, type MultiaddrParts, tuplesToBytes } from './codec.js'
2121
import { getProtocol, names } from './protocols-table.js'
22-
import { isMultiaddr, type AbortOptions, type MultiaddrInput, type Multiaddr as MultiaddrInterface, type MultiaddrObject, type Protocol, type StringTuple, type Tuple, resolvers, type NodeAddress } from './index.js'
22+
import { isMultiaddr, multiaddr, resolvers } from './index.js'
23+
import type { MultiaddrInput, Multiaddr as MultiaddrInterface, MultiaddrObject, Protocol, StringTuple, Tuple, NodeAddress, ResolveOptions } from './index.js'
2324

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

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

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

237-
const addresses = await resolver(this, options)
238-
return addresses.map((a) => new Multiaddr(a))
238+
const result = await resolver(this, options)
239+
240+
return result.map(str => multiaddr(str))
239241
}
240242

241243
nodeAddress (): NodeAddress {

src/resolvers/dns.browser.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/resolvers/dns.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/resolvers/dnsaddr.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { CodeError } from '@libp2p/interface'
2+
import { dns, RecordType } from '@multiformats/dns'
3+
import { raceSignal } from 'race-signal'
4+
import { multiaddr } from '../index.js'
5+
import { getProtocol } from '../protocols-table.js'
6+
import type { Resolver } from './index.js'
7+
import type { AbortOptions, Multiaddr } from '../index.js'
8+
import type { DNS } from '@multiformats/dns'
9+
10+
const MAX_RECURSIVE_DEPTH = 32
11+
const { code: dnsaddrCode } = getProtocol('dnsaddr')
12+
13+
export interface DNSADDROptions extends AbortOptions {
14+
/**
15+
* An optional DNS resolver
16+
*/
17+
dns?: DNS
18+
19+
/**
20+
* When resolving DNSADDR Multiaddrs that resolve to other DNSADDR Multiaddrs,
21+
* limit how many times we will recursively resolve them.
22+
*
23+
* @default 32
24+
*/
25+
maxRecursiveDepth?: number
26+
}
27+
28+
export const dnsaddrResolver: Resolver<DNSADDROptions> = async function dnsaddr (ma: Multiaddr, options: DNSADDROptions = {}): Promise<string[]> {
29+
const recursionLimit = options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH
30+
31+
if (recursionLimit === 0) {
32+
throw new CodeError('Max recursive depth reached', 'ERR_MAX_RECURSIVE_DEPTH_REACHED')
33+
}
34+
35+
const [, hostname] = ma.stringTuples().find(([proto]) => proto === dnsaddrCode) ?? []
36+
37+
const resolver = options?.dns ?? dns()
38+
const result = await raceSignal(resolver.query(`_dnsaddr.${hostname}`, {
39+
signal: options?.signal,
40+
types: [
41+
RecordType.TXT
42+
]
43+
}), options.signal)
44+
45+
const peerId = ma.getPeerId()
46+
const output: string[] = []
47+
48+
for (const answer of result.Answer) {
49+
const addr = answer.data.split('=')[1]
50+
51+
if (addr == null) {
52+
continue
53+
}
54+
55+
if (peerId != null && !addr.includes(peerId)) {
56+
continue
57+
}
58+
59+
const ma = multiaddr(addr)
60+
61+
if (addr.startsWith('/dnsaddr')) {
62+
const resolved = await ma.resolve({
63+
...options,
64+
maxRecursiveDepth: recursionLimit - 1
65+
})
66+
67+
output.push(...resolved.map(ma => ma.toString()))
68+
} else {
69+
output.push(ma.toString())
70+
}
71+
}
72+
73+
return output
74+
}

0 commit comments

Comments
 (0)