@libp2p/autonat 2.0.12 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.min.js +1 -1
- package/dist/src/autonat.d.ts +34 -7
- package/dist/src/autonat.d.ts.map +1 -1
- package/dist/src/autonat.js +389 -228
- package/dist/src/autonat.js.map +1 -1
- package/dist/src/constants.d.ts +1 -2
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +1 -2
- package/dist/src/constants.js.map +1 -1
- package/dist/src/index.d.ts +13 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/package.json +20 -18
- package/src/autonat.ts +489 -253
- package/src/constants.ts +1 -2
- package/src/index.ts +14 -1
- package/LICENSE +0 -4
package/src/autonat.ts
CHANGED
|
@@ -1,51 +1,114 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serviceCapabilities, serviceDependencies, setMaxListeners } from '@libp2p/interface'
|
|
2
|
+
import { peerSet } from '@libp2p/peer-collections'
|
|
2
3
|
import { peerIdFromMultihash } from '@libp2p/peer-id'
|
|
3
|
-
import {
|
|
4
|
+
import { createScalableCuckooFilter } from '@libp2p/utils/filters'
|
|
5
|
+
import { isGlobalUnicast } from '@libp2p/utils/multiaddr/is-global-unicast'
|
|
6
|
+
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
|
|
7
|
+
import { PeerQueue } from '@libp2p/utils/peer-queue'
|
|
8
|
+
import { repeatingTask } from '@libp2p/utils/repeating-task'
|
|
4
9
|
import { multiaddr, protocols } from '@multiformats/multiaddr'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import map from 'it-map'
|
|
8
|
-
import parallel from 'it-parallel'
|
|
9
|
-
import { pipe } from 'it-pipe'
|
|
10
|
+
import { anySignal } from 'any-signal'
|
|
11
|
+
import { pbStream } from 'it-protobuf-stream'
|
|
10
12
|
import * as Digest from 'multiformats/hashes/digest'
|
|
11
13
|
import {
|
|
12
|
-
|
|
13
|
-
MAX_OUTBOUND_STREAMS,
|
|
14
|
-
PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION, REFRESH_INTERVAL, STARTUP_DELAY, TIMEOUT
|
|
14
|
+
DEFAULT_CONNECTION_THRESHOLD,
|
|
15
|
+
MAX_INBOUND_STREAMS, MAX_OUTBOUND_STREAMS, PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION, TIMEOUT
|
|
15
16
|
} from './constants.js'
|
|
16
17
|
import { Message } from './pb/index.js'
|
|
17
18
|
import type { AutoNATComponents, AutoNATServiceInit } from './index.js'
|
|
18
|
-
import type { Logger, Connection, PeerId,
|
|
19
|
-
import type { IncomingStreamData } from '@libp2p/interface-internal'
|
|
19
|
+
import type { Logger, Connection, PeerId, Startable, AbortOptions } from '@libp2p/interface'
|
|
20
|
+
import type { AddressType, IncomingStreamData } from '@libp2p/interface-internal'
|
|
21
|
+
import type { PeerSet } from '@libp2p/peer-collections'
|
|
22
|
+
import type { Filter } from '@libp2p/utils/filters'
|
|
23
|
+
import type { RepeatingTask } from '@libp2p/utils/repeating-task'
|
|
24
|
+
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
20
25
|
|
|
21
26
|
// if more than 3 peers manage to dial us on what we believe to be our external
|
|
22
27
|
// IP then we are convinced that it is, in fact, our external IP
|
|
23
|
-
// https://github.com/libp2p/specs/blob/master/autonat/
|
|
28
|
+
// https://github.com/libp2p/specs/blob/master/autonat/autonat-v1.md#autonat-protocol
|
|
24
29
|
const REQUIRED_SUCCESSFUL_DIALS = 4
|
|
30
|
+
const REQUIRED_FAILED_DIALS = 8
|
|
31
|
+
|
|
32
|
+
interface TestAddressOptions extends AbortOptions {
|
|
33
|
+
multiaddr: Multiaddr
|
|
34
|
+
peerId: PeerId
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DialResults {
|
|
38
|
+
/**
|
|
39
|
+
* The address being tested
|
|
40
|
+
*/
|
|
41
|
+
multiaddr: Multiaddr
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The number of successful dials from peers
|
|
45
|
+
*/
|
|
46
|
+
success: number
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The number of dial failures from peers
|
|
50
|
+
*/
|
|
51
|
+
failure: number
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* For the multiaddr corresponding the the string key of the `dialResults`
|
|
55
|
+
* map, these are the IP segments that a successful dial result has been
|
|
56
|
+
* received from
|
|
57
|
+
*/
|
|
58
|
+
networkSegments: string[]
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure that the same peer id can't verify multiple times
|
|
62
|
+
*/
|
|
63
|
+
verifyingPeers: PeerSet
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The number of peers currently verifying this address
|
|
67
|
+
*/
|
|
68
|
+
queue: PeerQueue<void, TestAddressOptions>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Updated when this address is verified or failed
|
|
72
|
+
*/
|
|
73
|
+
result?: boolean
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The type of address
|
|
77
|
+
*/
|
|
78
|
+
type: AddressType
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The last time the address was verified
|
|
82
|
+
*/
|
|
83
|
+
lastVerified?: number
|
|
84
|
+
}
|
|
25
85
|
|
|
26
86
|
export class AutoNATService implements Startable {
|
|
27
87
|
private readonly components: AutoNATComponents
|
|
28
|
-
private readonly startupDelay: number
|
|
29
|
-
private readonly refreshInterval: number
|
|
30
88
|
private readonly protocol: string
|
|
31
89
|
private readonly timeout: number
|
|
32
90
|
private readonly maxInboundStreams: number
|
|
33
91
|
private readonly maxOutboundStreams: number
|
|
34
|
-
private verifyAddressTimeout?: ReturnType<typeof setTimeout>
|
|
35
92
|
private started: boolean
|
|
36
93
|
private readonly log: Logger
|
|
94
|
+
private topologyId?: string
|
|
95
|
+
private readonly dialResults: Map<string, DialResults>
|
|
96
|
+
private readonly findPeers: RepeatingTask
|
|
97
|
+
private readonly addressFilter: Filter
|
|
98
|
+
private readonly connectionThreshold: number
|
|
37
99
|
|
|
38
100
|
constructor (components: AutoNATComponents, init: AutoNATServiceInit) {
|
|
39
101
|
this.components = components
|
|
40
|
-
this.log = components.logger.forComponent('libp2p:
|
|
102
|
+
this.log = components.logger.forComponent('libp2p:auto-nat')
|
|
41
103
|
this.started = false
|
|
42
104
|
this.protocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`
|
|
43
105
|
this.timeout = init.timeout ?? TIMEOUT
|
|
44
106
|
this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS
|
|
45
107
|
this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
108
|
+
this.connectionThreshold = init.connectionThreshold ?? DEFAULT_CONNECTION_THRESHOLD
|
|
109
|
+
this.dialResults = new Map()
|
|
110
|
+
this.findPeers = repeatingTask(this.findRandomPeers.bind(this), 60_000)
|
|
111
|
+
this.addressFilter = createScalableCuckooFilter(1024)
|
|
49
112
|
}
|
|
50
113
|
|
|
51
114
|
readonly [Symbol.toStringTag] = '@libp2p/autonat'
|
|
@@ -54,6 +117,12 @@ export class AutoNATService implements Startable {
|
|
|
54
117
|
'@libp2p/autonat'
|
|
55
118
|
]
|
|
56
119
|
|
|
120
|
+
get [serviceDependencies] (): string[] {
|
|
121
|
+
return [
|
|
122
|
+
'@libp2p/identify'
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
57
126
|
isStarted (): boolean {
|
|
58
127
|
return this.started
|
|
59
128
|
}
|
|
@@ -66,100 +135,119 @@ export class AutoNATService implements Startable {
|
|
|
66
135
|
await this.components.registrar.handle(this.protocol, (data) => {
|
|
67
136
|
void this.handleIncomingAutonatStream(data)
|
|
68
137
|
.catch(err => {
|
|
69
|
-
this.log.error('error handling incoming autonat stream', err)
|
|
138
|
+
this.log.error('error handling incoming autonat stream - %e', err)
|
|
70
139
|
})
|
|
71
140
|
}, {
|
|
72
141
|
maxInboundStreams: this.maxInboundStreams,
|
|
73
142
|
maxOutboundStreams: this.maxOutboundStreams
|
|
74
143
|
})
|
|
75
144
|
|
|
76
|
-
this.
|
|
145
|
+
this.topologyId = await this.components.registrar.register(this.protocol, {
|
|
146
|
+
onConnect: (peerId, connection) => {
|
|
147
|
+
this.verifyExternalAddresses(connection)
|
|
148
|
+
.catch(err => {
|
|
149
|
+
this.log.error('could not verify addresses - %e', err)
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
})
|
|
77
153
|
|
|
154
|
+
this.findPeers.start()
|
|
78
155
|
this.started = true
|
|
79
156
|
}
|
|
80
157
|
|
|
81
158
|
async stop (): Promise<void> {
|
|
82
159
|
await this.components.registrar.unhandle(this.protocol)
|
|
83
|
-
clearTimeout(this.verifyAddressTimeout)
|
|
84
160
|
|
|
161
|
+
if (this.topologyId != null) {
|
|
162
|
+
await this.components.registrar.unhandle(this.topologyId)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.dialResults.clear()
|
|
166
|
+
this.findPeers.stop()
|
|
85
167
|
this.started = false
|
|
86
168
|
}
|
|
87
169
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
170
|
+
private allAddressesAreVerified (): boolean {
|
|
171
|
+
return this.components.addressManager.getAddressesWithMetadata().every(addr => {
|
|
172
|
+
if (addr.expires > Date.now()) {
|
|
173
|
+
// ignore any unverified addresses within their TTL
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
93
176
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
177
|
+
return addr.verified
|
|
178
|
+
})
|
|
179
|
+
}
|
|
97
180
|
|
|
98
|
-
|
|
181
|
+
async findRandomPeers (options?: AbortOptions): Promise<void> {
|
|
182
|
+
// skip if all addresses are verified
|
|
183
|
+
if (this.allAddressesAreVerified()) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
99
186
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
187
|
+
const signal = anySignal([
|
|
188
|
+
AbortSignal.timeout(10_000),
|
|
189
|
+
options?.signal
|
|
190
|
+
])
|
|
103
191
|
|
|
192
|
+
// spend a few seconds finding random peers - dial them which will run
|
|
193
|
+
// identify to trigger the topology callbacks and run AutoNAT
|
|
104
194
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await pipe(
|
|
108
|
-
data.stream,
|
|
109
|
-
(source) => lp.decode(source),
|
|
110
|
-
async function * (stream) {
|
|
111
|
-
const buf = await first(stream)
|
|
112
|
-
|
|
113
|
-
if (buf == null) {
|
|
114
|
-
self.log('no message received')
|
|
115
|
-
yield Message.encode({
|
|
116
|
-
type: Message.MessageType.DIAL_RESPONSE,
|
|
117
|
-
dialResponse: {
|
|
118
|
-
status: Message.ResponseStatus.E_BAD_REQUEST,
|
|
119
|
-
statusText: 'No message was sent'
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
}
|
|
195
|
+
this.log('starting random walk to find peers to run AutoNAT')
|
|
125
196
|
|
|
126
|
-
|
|
197
|
+
for await (const peer of this.components.randomWalk.walk({ signal })) {
|
|
198
|
+
if (!(await this.components.connectionManager.isDialable(peer.multiaddrs))) {
|
|
199
|
+
this.log.trace('random peer %p was not dialable %s', peer.id, peer.multiaddrs.map(ma => ma.toString()).join(', '))
|
|
127
200
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
self.log.error('could not decode message', err)
|
|
201
|
+
// skip peers we can't dial
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
132
204
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
205
|
+
try {
|
|
206
|
+
this.log.trace('dial random peer %p', peer.id)
|
|
207
|
+
await this.components.connectionManager.openConnection(peer.multiaddrs, {
|
|
208
|
+
signal
|
|
209
|
+
})
|
|
210
|
+
} catch {}
|
|
140
211
|
|
|
141
|
-
|
|
142
|
-
|
|
212
|
+
if (this.allAddressesAreVerified()) {
|
|
213
|
+
this.log('stopping random walk, all addresses are verified')
|
|
214
|
+
return
|
|
215
|
+
}
|
|
143
216
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
} catch (err) {
|
|
152
|
-
this.log.error('error handling incoming autonat stream', err)
|
|
153
|
-
} finally {
|
|
154
|
-
signal.removeEventListener('abort', onAbort)
|
|
155
|
-
}
|
|
217
|
+
if (!this.hasConnectionCapacity()) {
|
|
218
|
+
this.log('stopping random walk, too close to max connections')
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
156
223
|
}
|
|
157
224
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Handle an incoming AutoNAT request
|
|
227
|
+
*/
|
|
228
|
+
async handleIncomingAutonatStream (data: IncomingStreamData): Promise<void> {
|
|
229
|
+
const signal = AbortSignal.timeout(this.timeout)
|
|
230
|
+
setMaxListeners(Infinity, signal)
|
|
231
|
+
|
|
232
|
+
const messages = pbStream(data.stream).pb(Message)
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const request = await messages.read({
|
|
236
|
+
signal
|
|
162
237
|
})
|
|
238
|
+
const response = await this.handleAutonatMessage(request, data.connection, {
|
|
239
|
+
signal
|
|
240
|
+
})
|
|
241
|
+
await messages.write(response, {
|
|
242
|
+
signal
|
|
243
|
+
})
|
|
244
|
+
await messages.unwrap().unwrap().close({
|
|
245
|
+
signal
|
|
246
|
+
})
|
|
247
|
+
} catch (err: any) {
|
|
248
|
+
this.log.error('error handling incoming autonat stream - %e', err)
|
|
249
|
+
data.stream.abort(err)
|
|
250
|
+
}
|
|
163
251
|
}
|
|
164
252
|
|
|
165
253
|
private async handleAutonatMessage (message: Message, connection: Connection, options?: AbortOptions): Promise<Message> {
|
|
@@ -199,7 +287,7 @@ export class AutoNATService implements Startable {
|
|
|
199
287
|
const digest = Digest.decode(peer.id)
|
|
200
288
|
peerId = peerIdFromMultihash(digest)
|
|
201
289
|
} catch (err) {
|
|
202
|
-
this.log.error('invalid PeerId', err)
|
|
290
|
+
this.log.error('invalid PeerId - %e', err)
|
|
203
291
|
|
|
204
292
|
return {
|
|
205
293
|
type: Message.MessageType.DIAL_RESPONSE,
|
|
@@ -229,34 +317,31 @@ export class AutoNATService implements Startable {
|
|
|
229
317
|
const multiaddrs = peer.addrs
|
|
230
318
|
.map(buf => multiaddr(buf))
|
|
231
319
|
.filter(ma => {
|
|
232
|
-
const
|
|
320
|
+
const options = ma.toOptions()
|
|
233
321
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
.filter(ma => {
|
|
239
|
-
const host = ma.toOptions().host
|
|
240
|
-
const isPublicIp = !(isPrivateIp(host) ?? false)
|
|
322
|
+
if (isPrivate(ma)) {
|
|
323
|
+
// don't try to dial private addresses
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
241
326
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const host = ma.toOptions().host
|
|
248
|
-
const isNotOurHost = !ourHosts.includes(host)
|
|
327
|
+
if (options.host !== connection.remoteAddr.toOptions().host) {
|
|
328
|
+
// skip any Multiaddrs where the target node's IP does not match the sending node's IP
|
|
329
|
+
this.log.trace('not dialing %a - target host did not match remote host %a', ma, connection.remoteAddr)
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
249
332
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.filter(ma => {
|
|
255
|
-
const isSupportedTransport = Boolean(this.components.transportManager.dialTransportForMultiaddr(ma))
|
|
333
|
+
if (ourHosts.includes(options.host)) {
|
|
334
|
+
// don't try to dial nodes on the same host as us
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
256
337
|
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
338
|
+
if (this.components.transportManager.dialTransportForMultiaddr(ma) == null) {
|
|
339
|
+
// skip any Multiaddrs that have transports we do not support
|
|
340
|
+
this.log.trace('not dialing %a - transport unsupported', ma)
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return true
|
|
260
345
|
})
|
|
261
346
|
.map(ma => {
|
|
262
347
|
if (ma.getPeerId() == null) {
|
|
@@ -269,7 +354,7 @@ export class AutoNATService implements Startable {
|
|
|
269
354
|
|
|
270
355
|
// make sure we have something to dial
|
|
271
356
|
if (multiaddrs.length === 0) {
|
|
272
|
-
this.log('
|
|
357
|
+
this.log('refused to dial all multiaddrs for %p from message', peerId)
|
|
273
358
|
|
|
274
359
|
return {
|
|
275
360
|
type: Message.MessageType.DIAL_RESPONSE,
|
|
@@ -297,7 +382,7 @@ export class AutoNATService implements Startable {
|
|
|
297
382
|
throw new Error('Unexpected remote address')
|
|
298
383
|
}
|
|
299
384
|
|
|
300
|
-
this.log('
|
|
385
|
+
this.log('successfully dialed %p via %a', peerId, multiaddr)
|
|
301
386
|
|
|
302
387
|
return {
|
|
303
388
|
type: Message.MessageType.DIAL_RESPONSE,
|
|
@@ -307,7 +392,7 @@ export class AutoNATService implements Startable {
|
|
|
307
392
|
}
|
|
308
393
|
}
|
|
309
394
|
} catch (err: any) {
|
|
310
|
-
this.log('could not dial %p', peerId, err)
|
|
395
|
+
this.log.error('could not dial %p - %e', peerId, err)
|
|
311
396
|
errorMessage = err.message
|
|
312
397
|
} finally {
|
|
313
398
|
if (connection != null) {
|
|
@@ -327,191 +412,342 @@ export class AutoNATService implements Startable {
|
|
|
327
412
|
}
|
|
328
413
|
|
|
329
414
|
/**
|
|
330
|
-
*
|
|
415
|
+
* The AutoNAT v1 server is not required to send us the address that it
|
|
416
|
+
* dialed successfully.
|
|
417
|
+
*
|
|
418
|
+
* When addresses fail, it can be because they are NATed, or because the peer
|
|
419
|
+
* did't support the transport, we have no way of knowing, so just send them
|
|
420
|
+
* one address so we can treat the response as:
|
|
421
|
+
*
|
|
422
|
+
* - OK - the dial request worked and the address is not NATed
|
|
423
|
+
* - E_DIAL_ERROR - the dial request failed and the address may be NATed
|
|
424
|
+
* - E_DIAL_REFUSED/E_BAD_REQUEST/E_INTERNAL_ERROR - the remote didn't dial the address
|
|
425
|
+
*/
|
|
426
|
+
private getFirstUnverifiedMultiaddr (segment: string, supportsIPv6: boolean): DialResults | undefined {
|
|
427
|
+
const addrs = this.components.addressManager.getAddressesWithMetadata()
|
|
428
|
+
.sort((a, b) => {
|
|
429
|
+
// sort addresses, de-prioritize observed addresses
|
|
430
|
+
if (a.type === 'observed' && b.type !== 'observed') {
|
|
431
|
+
return 1
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (b.type === 'observed' && a.type !== 'observed') {
|
|
435
|
+
return -1
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return 0
|
|
439
|
+
})
|
|
440
|
+
.filter(addr => {
|
|
441
|
+
const expired = addr.expires < Date.now()
|
|
442
|
+
|
|
443
|
+
if (!expired) {
|
|
444
|
+
// skip verified/non-verified addresses within their TTL
|
|
445
|
+
return false
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const options = addr.multiaddr.toOptions()
|
|
449
|
+
|
|
450
|
+
if (options.family === 6) {
|
|
451
|
+
// do not send IPv6 addresses to peers without IPv6 addresses
|
|
452
|
+
if (!supportsIPv6) {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!isGlobalUnicast(addr.multiaddr)) {
|
|
457
|
+
// skip non-globally routable addresses
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (isPrivate(addr.multiaddr)) {
|
|
463
|
+
// skip private addresses
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return true
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
for (const addr of addrs) {
|
|
471
|
+
const addrString = addr.multiaddr.toString()
|
|
472
|
+
let results = this.dialResults.get(addrString)
|
|
473
|
+
|
|
474
|
+
if (results != null) {
|
|
475
|
+
if (results.networkSegments.includes(segment)) {
|
|
476
|
+
this.log.trace('%a already has a network segment result from %s', results.multiaddr, segment)
|
|
477
|
+
// skip this address if we already have a dial result from the
|
|
478
|
+
// network segment the peer is in
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (results.queue.size > 10) {
|
|
483
|
+
this.log.trace('%a already has enough peers queued', results.multiaddr)
|
|
484
|
+
// already have enough peers verifying this address, skip on to the
|
|
485
|
+
// next one
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// will include this multiaddr, ensure we have a results object
|
|
491
|
+
if (results == null) {
|
|
492
|
+
const needsRevalidating = addr.expires < Date.now()
|
|
493
|
+
|
|
494
|
+
// allow re-validating addresses that worked previously
|
|
495
|
+
if (needsRevalidating) {
|
|
496
|
+
this.addressFilter.remove?.(addrString)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (this.addressFilter.has(addrString)) {
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// only try to validate the address once
|
|
504
|
+
this.addressFilter.add(addrString)
|
|
505
|
+
|
|
506
|
+
this.log.trace('creating dial result %s %s', needsRevalidating ? 'to revalidate' : 'for', addrString)
|
|
507
|
+
results = {
|
|
508
|
+
multiaddr: addr.multiaddr,
|
|
509
|
+
success: 0,
|
|
510
|
+
failure: 0,
|
|
511
|
+
networkSegments: [],
|
|
512
|
+
verifyingPeers: peerSet(),
|
|
513
|
+
queue: new PeerQueue({
|
|
514
|
+
concurrency: 3,
|
|
515
|
+
maxSize: 50
|
|
516
|
+
}),
|
|
517
|
+
type: addr.type,
|
|
518
|
+
lastVerified: addr.lastVerified
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this.dialResults.set(addrString, results)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return results
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Removes any multiaddr result objects created for old multiaddrs that we are
|
|
530
|
+
* no longer waiting on
|
|
331
531
|
*/
|
|
332
|
-
|
|
333
|
-
|
|
532
|
+
private removeOutdatedMultiaddrResults (): void {
|
|
533
|
+
const unverifiedMultiaddrs = new Set(this.components.addressManager.getAddressesWithMetadata()
|
|
534
|
+
.filter(({ expires }) => {
|
|
535
|
+
if (expires < Date.now()) {
|
|
536
|
+
return true
|
|
537
|
+
}
|
|
334
538
|
|
|
335
|
-
|
|
539
|
+
return false
|
|
540
|
+
})
|
|
541
|
+
.map(({ multiaddr }) => multiaddr.toString())
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
for (const multiaddr of this.dialResults.keys()) {
|
|
545
|
+
if (!unverifiedMultiaddrs.has(multiaddr)) {
|
|
546
|
+
this.log.trace('remove results for %a', multiaddr)
|
|
547
|
+
this.dialResults.delete(multiaddr)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Our multicodec topology noticed a new peer that supports autonat
|
|
554
|
+
*/
|
|
555
|
+
async verifyExternalAddresses (connection: Connection): Promise<void> {
|
|
556
|
+
// do nothing if we are not running
|
|
336
557
|
if (!this.isStarted()) {
|
|
337
558
|
return
|
|
338
559
|
}
|
|
339
560
|
|
|
340
|
-
|
|
561
|
+
// perform cleanup
|
|
562
|
+
this.removeOutdatedMultiaddrResults()
|
|
341
563
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
564
|
+
const peer = await this.components.peerStore.get(connection.remotePeer)
|
|
565
|
+
|
|
566
|
+
// if the remote peer has IPv6 addresses, we can probably send them an IPv6
|
|
567
|
+
// address to verify, otherwise only send them IPv4 addresses
|
|
568
|
+
const supportsIPv6 = peer.addresses.some(({ multiaddr }) => {
|
|
569
|
+
return multiaddr.toOptions().family === 6
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
// get multiaddrs this peer is eligible to verify
|
|
573
|
+
const segment = this.getNetworkSegment(connection.remoteAddr)
|
|
574
|
+
const results = this.getFirstUnverifiedMultiaddr(segment, supportsIPv6)
|
|
345
575
|
|
|
346
|
-
|
|
576
|
+
if (results == null) {
|
|
577
|
+
this.log.trace('no unverified public addresses found for peer %p to verify, not requesting verification', connection.remotePeer)
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!this.hasConnectionCapacity()) {
|
|
582
|
+
// we are near the max connection limit - any dial attempts from remote
|
|
583
|
+
// peers may be rejected which will get flagged as false dial errors and
|
|
584
|
+
// lead us to un-verify an otherwise reachable address
|
|
585
|
+
|
|
586
|
+
if (results.lastVerified != null) {
|
|
587
|
+
this.log('automatically re-verifying %a because we are too close to the connection limit', results.multiaddr)
|
|
588
|
+
this.confirmAddress(results)
|
|
589
|
+
} else {
|
|
590
|
+
this.log('skipping verifying %a because we are too close to the connection limit', results.multiaddr)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
results.queue.add(async (options: TestAddressOptions) => {
|
|
597
|
+
await this.askPeerToVerify(connection, segment, options)
|
|
598
|
+
}, {
|
|
599
|
+
peerId: connection.remotePeer,
|
|
600
|
+
multiaddr: results.multiaddr
|
|
601
|
+
})
|
|
602
|
+
.catch(err => {
|
|
603
|
+
if (results?.result == null) {
|
|
604
|
+
this.log.error('error from %p verifying address %a - %e', connection.remotePeer, results?.multiaddr, err)
|
|
605
|
+
}
|
|
347
606
|
})
|
|
607
|
+
}
|
|
348
608
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.refreshInterval)
|
|
609
|
+
private async askPeerToVerify (connection: Connection, segment: string, options: TestAddressOptions): Promise<void> {
|
|
610
|
+
let results = this.dialResults.get(options.multiaddr.toString())
|
|
352
611
|
|
|
612
|
+
if (results == null) {
|
|
613
|
+
this.log('%a was verified while %p was queued', options.multiaddr, connection.remotePeer)
|
|
353
614
|
return
|
|
354
615
|
}
|
|
355
616
|
|
|
356
617
|
const signal = AbortSignal.timeout(this.timeout)
|
|
357
|
-
|
|
358
|
-
// this controller may be used while dialing lots of peers so prevent MaxListenersExceededWarning
|
|
359
|
-
// appearing in the console
|
|
360
618
|
setMaxListeners(Infinity, signal)
|
|
361
619
|
|
|
362
|
-
|
|
620
|
+
this.log.trace('asking %p to verify multiaddr %s', connection.remotePeer, options.multiaddr)
|
|
621
|
+
|
|
622
|
+
const stream = await connection.newStream(this.protocol, {
|
|
623
|
+
signal
|
|
624
|
+
})
|
|
363
625
|
|
|
364
626
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
627
|
+
const messages = pbStream(stream).pb(Message)
|
|
628
|
+
const [, response] = await Promise.all([
|
|
629
|
+
messages.write({
|
|
630
|
+
type: Message.MessageType.DIAL,
|
|
631
|
+
dial: {
|
|
632
|
+
peer: {
|
|
633
|
+
id: this.components.peerId.toMultihash().bytes,
|
|
634
|
+
addrs: [options.multiaddr.bytes]
|
|
635
|
+
}
|
|
373
636
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
637
|
+
}, { signal }),
|
|
638
|
+
messages.read({ signal })
|
|
639
|
+
])
|
|
376
640
|
|
|
377
|
-
|
|
378
|
-
|
|
641
|
+
if (response.type !== Message.MessageType.DIAL_RESPONSE || response.dialResponse == null) {
|
|
642
|
+
this.log('invalid autonat response from %p - %j', connection.remotePeer, response)
|
|
643
|
+
return
|
|
644
|
+
}
|
|
379
645
|
|
|
380
|
-
const
|
|
381
|
-
let onAbort = (): void => {}
|
|
646
|
+
const status = response.dialResponse.status
|
|
382
647
|
|
|
383
|
-
|
|
384
|
-
this.log('asking %p to verify multiaddr', peer.id)
|
|
648
|
+
this.log.trace('autonat response from %p for %a is %s', connection.remotePeer, options.multiaddr, status)
|
|
385
649
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const stream = await connection.newStream(this.protocol, {
|
|
391
|
-
signal
|
|
392
|
-
})
|
|
650
|
+
if (status !== Message.ResponseStatus.OK && status !== Message.ResponseStatus.E_DIAL_ERROR) {
|
|
651
|
+
return
|
|
652
|
+
}
|
|
393
653
|
|
|
394
|
-
|
|
654
|
+
results = this.dialResults.get(options.multiaddr.toString())
|
|
395
655
|
|
|
396
|
-
|
|
656
|
+
if (results == null) {
|
|
657
|
+
this.log.trace('peer reported %a as %s but there is no result object', options.multiaddr, response.dialResponse.status)
|
|
658
|
+
return
|
|
659
|
+
}
|
|
397
660
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
(source) => lp.decode(source),
|
|
403
|
-
async (stream) => first(stream)
|
|
404
|
-
)
|
|
405
|
-
if (buf == null) {
|
|
406
|
-
this.log('no response received from %p', connection.remotePeer)
|
|
407
|
-
return undefined
|
|
408
|
-
}
|
|
409
|
-
const response = Message.decode(buf)
|
|
661
|
+
if (results.networkSegments.includes(segment)) {
|
|
662
|
+
this.log.trace('%a results included network segment %s', options.multiaddr, segment)
|
|
663
|
+
return
|
|
664
|
+
}
|
|
410
665
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
666
|
+
if (results.result != null) {
|
|
667
|
+
this.log.trace('already resolved result for %a, ignoring response from', options.multiaddr, connection.remotePeer)
|
|
668
|
+
return
|
|
669
|
+
}
|
|
415
670
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (options.family === 4) {
|
|
422
|
-
const octets = options.host.split('.')
|
|
423
|
-
segment = octets[0]
|
|
424
|
-
} else if (options.family === 6) {
|
|
425
|
-
const octets = options.host.split(':')
|
|
426
|
-
segment = octets[0]
|
|
427
|
-
} else {
|
|
428
|
-
this.log('remote address "%s" was not IP4 or IP6?', options.host)
|
|
429
|
-
return undefined
|
|
430
|
-
}
|
|
671
|
+
if (results.verifyingPeers.has(connection.remotePeer)) {
|
|
672
|
+
this.log.trace('peer %p has already verified %a, ignoring response', connection.remotePeer, options.multiaddr)
|
|
673
|
+
return
|
|
674
|
+
}
|
|
431
675
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
return undefined
|
|
435
|
-
}
|
|
676
|
+
results.verifyingPeers.add(connection.remotePeer)
|
|
677
|
+
results.networkSegments.push(segment)
|
|
436
678
|
|
|
437
|
-
|
|
438
|
-
|
|
679
|
+
if (status === Message.ResponseStatus.OK) {
|
|
680
|
+
results.success++
|
|
439
681
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
this.
|
|
443
|
-
|
|
444
|
-
signal.removeEventListener('abort', onAbort)
|
|
682
|
+
// observed addresses require more confirmations
|
|
683
|
+
if (results.type !== 'observed') {
|
|
684
|
+
this.confirmAddress(results)
|
|
685
|
+
return
|
|
445
686
|
}
|
|
687
|
+
} else if (status === Message.ResponseStatus.E_DIAL_ERROR) {
|
|
688
|
+
results.failure++
|
|
446
689
|
}
|
|
447
690
|
|
|
448
|
-
|
|
449
|
-
for await (const dialResponse of parallel(map(this.components.randomWalk.walk({
|
|
450
|
-
signal
|
|
451
|
-
}), (peer) => async () => verifyAddress(peer)), {
|
|
452
|
-
concurrency: REQUIRED_SUCCESSFUL_DIALS
|
|
453
|
-
})) {
|
|
454
|
-
try {
|
|
455
|
-
if (dialResponse == null) {
|
|
456
|
-
continue
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// they either told us which address worked/didn't work, or we only sent them one address
|
|
460
|
-
const addr = dialResponse.addr == null ? multiaddrs[0] : multiaddr(dialResponse.addr)
|
|
691
|
+
this.log('%a success %d failure %d', results.multiaddr, results.success, results.failure)
|
|
461
692
|
|
|
462
|
-
|
|
693
|
+
if (results.success === REQUIRED_SUCCESSFUL_DIALS) {
|
|
694
|
+
this.confirmAddress(results)
|
|
695
|
+
}
|
|
463
696
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
697
|
+
if (results.failure === REQUIRED_FAILED_DIALS) {
|
|
698
|
+
this.unconfirmAddress(results)
|
|
699
|
+
}
|
|
700
|
+
} finally {
|
|
701
|
+
try {
|
|
702
|
+
await stream.close({
|
|
703
|
+
signal
|
|
704
|
+
})
|
|
705
|
+
} catch (err: any) {
|
|
706
|
+
stream.abort(err)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
468
710
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
711
|
+
private hasConnectionCapacity (): boolean {
|
|
712
|
+
const connections = this.components.connectionManager.getConnections()
|
|
713
|
+
const currentConnectionCount = connections.length
|
|
714
|
+
const maxConnections = this.components.connectionManager.getMaxConnections()
|
|
473
715
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
continue
|
|
477
|
-
}
|
|
716
|
+
return ((currentConnectionCount / maxConnections) * 100) < this.connectionThreshold
|
|
717
|
+
}
|
|
478
718
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
719
|
+
private confirmAddress (results: DialResults): void {
|
|
720
|
+
// we are now convinced
|
|
721
|
+
this.log('%s address %a is externally dialable', results.type, results.multiaddr)
|
|
722
|
+
this.components.addressManager.confirmObservedAddr(results.multiaddr)
|
|
723
|
+
this.dialResults.delete(results.multiaddr.toString())
|
|
483
724
|
|
|
484
|
-
|
|
725
|
+
// abort & remove any outstanding verification jobs for this multiaddr
|
|
726
|
+
results.result = true
|
|
727
|
+
results.queue.abort()
|
|
728
|
+
}
|
|
485
729
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
730
|
+
private unconfirmAddress (results: DialResults): void {
|
|
731
|
+
// we are now unconvinced
|
|
732
|
+
this.log('%s address %a is not externally dialable', results.type, results.multiaddr)
|
|
733
|
+
this.components.addressManager.removeObservedAddr(results.multiaddr)
|
|
734
|
+
this.dialResults.delete(results.multiaddr.toString())
|
|
489
735
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
736
|
+
// abort & remove any outstanding verification jobs for this multiaddr
|
|
737
|
+
results.result = false
|
|
738
|
+
results.queue.abort()
|
|
739
|
+
}
|
|
495
740
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
addressManager.confirmObservedAddr(addr)
|
|
500
|
-
return
|
|
501
|
-
}
|
|
741
|
+
private getNetworkSegment (ma: Multiaddr): string {
|
|
742
|
+
// make sure we use different network segments
|
|
743
|
+
const options = ma.toOptions()
|
|
502
744
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
addressManager.removeObservedAddr(addr)
|
|
507
|
-
return
|
|
508
|
-
}
|
|
509
|
-
} catch (err) {
|
|
510
|
-
this.log.error('could not verify external address', err)
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
} finally {
|
|
514
|
-
this.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.refreshInterval)
|
|
745
|
+
if (options.family === 4) {
|
|
746
|
+
const octets = options.host.split('.')
|
|
747
|
+
return octets[0].padStart(3, '0')
|
|
515
748
|
}
|
|
749
|
+
|
|
750
|
+
const octets = options.host.split(':')
|
|
751
|
+
return octets[0].padStart(4, '0')
|
|
516
752
|
}
|
|
517
753
|
}
|