@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/src/autonat.ts CHANGED
@@ -1,51 +1,114 @@
1
- import { AbortError, serviceCapabilities, setMaxListeners } from '@libp2p/interface'
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 { isPrivateIp } from '@libp2p/utils/private-ip'
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 first from 'it-first'
6
- import * as lp from 'it-length-prefixed'
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
- MAX_INBOUND_STREAMS,
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, PeerInfo, Startable, AbortOptions } from '@libp2p/interface'
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/README.md#autonat-protocol
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:autonat')
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.startupDelay = init.startupDelay ?? STARTUP_DELAY
47
- this.refreshInterval = init.refreshInterval ?? REFRESH_INTERVAL
48
- this._verifyExternalAddresses = this._verifyExternalAddresses.bind(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.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.startupDelay)
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
- * Handle an incoming AutoNAT request
90
- */
91
- async handleIncomingAutonatStream (data: IncomingStreamData): Promise<void> {
92
- const signal = AbortSignal.timeout(this.timeout)
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
- const onAbort = (): void => {
95
- data.stream.abort(new AbortError())
96
- }
177
+ return addr.verified
178
+ })
179
+ }
97
180
 
98
- signal.addEventListener('abort', onAbort, { once: true })
181
+ async findRandomPeers (options?: AbortOptions): Promise<void> {
182
+ // skip if all addresses are verified
183
+ if (this.allAddressesAreVerified()) {
184
+ return
185
+ }
99
186
 
100
- // this controller may be used while dialing lots of peers so prevent MaxListenersExceededWarning
101
- // appearing in the console
102
- setMaxListeners(Infinity, signal)
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
- const self = this
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
- let request: Message
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
- try {
129
- request = Message.decode(buf)
130
- } catch (err) {
131
- self.log.error('could not decode message', err)
201
+ // skip peers we can't dial
202
+ continue
203
+ }
132
204
 
133
- yield Message.encode({
134
- type: Message.MessageType.DIAL_RESPONSE,
135
- dialResponse: {
136
- status: Message.ResponseStatus.E_BAD_REQUEST,
137
- statusText: 'Could not decode message'
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
- return
142
- }
212
+ if (this.allAddressesAreVerified()) {
213
+ this.log('stopping random walk, all addresses are verified')
214
+ return
215
+ }
143
216
 
144
- yield Message.encode(await self.handleAutonatMessage(request, data.connection, {
145
- signal
146
- }))
147
- },
148
- (source) => lp.encode(source),
149
- data.stream
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
- _verifyExternalAddresses (): void {
159
- void this.verifyExternalAddresses()
160
- .catch(err => {
161
- this.log.error('error verifying external address', err)
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 isFromSameHost = ma.toOptions().host === connection.remoteAddr.toOptions().host
320
+ const options = ma.toOptions()
233
321
 
234
- this.log.trace('request to dial %a was sent from %a is same host %s', ma, connection.remoteAddr, isFromSameHost)
235
- // skip any Multiaddrs where the target node's IP does not match the sending node's IP
236
- return isFromSameHost
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
- this.log.trace('host %s was public %s', host, isPublicIp)
243
- // don't try to dial private addresses
244
- return isPublicIp
245
- })
246
- .filter(ma => {
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
- this.log.trace('host %s was not our host %s', host, isNotOurHost)
251
- // don't try to dial nodes on the same host as us
252
- return isNotOurHost
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.log.trace('transport for %a is supported %s', ma, isSupportedTransport)
258
- // skip any Multiaddrs that have transports we do not support
259
- return isSupportedTransport
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('no valid multiaddrs for %p in message', peerId)
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('Success %p', peerId)
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
- * Our multicodec topology noticed a new peer that supports autonat
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
- async verifyExternalAddresses (): Promise<void> {
333
- clearTimeout(this.verifyAddressTimeout)
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
- // Do not try to push if we are not running
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
- const addressManager = this.components.addressManager
561
+ // perform cleanup
562
+ this.removeOutdatedMultiaddrResults()
341
563
 
342
- const multiaddrs = addressManager.getObservedAddrs()
343
- .filter(ma => {
344
- const options = ma.toOptions()
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
- return !(isPrivateIp(options.host) ?? false)
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
- if (multiaddrs.length === 0) {
350
- this.log('no public addresses found, not requesting verification')
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
- const self = this
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
- this.log('verify multiaddrs %s', multiaddrs.map(ma => ma.toString()).join(', '))
366
-
367
- const request = Message.encode({
368
- type: Message.MessageType.DIAL,
369
- dial: {
370
- peer: {
371
- id: this.components.peerId.toMultihash().bytes,
372
- addrs: multiaddrs.map(map => map.bytes)
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
- const results: Record<string, { success: number, failure: number }> = {}
378
- const networkSegments: string[] = []
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 verifyAddress = async (peer: PeerInfo): Promise<Message.DialResponse | undefined> => {
381
- let onAbort = (): void => {}
646
+ const status = response.dialResponse.status
382
647
 
383
- try {
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
- const connection = await self.components.connectionManager.openConnection(peer.id, {
387
- signal
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
- onAbort = () => { stream.abort(new AbortError()) }
654
+ results = this.dialResults.get(options.multiaddr.toString())
395
655
 
396
- signal.addEventListener('abort', onAbort, { once: true })
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
- const buf = await pipe(
399
- [request],
400
- (source) => lp.encode(source),
401
- stream,
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
- if (response.type !== Message.MessageType.DIAL_RESPONSE || response.dialResponse == null) {
412
- this.log('invalid autonat response from %p', connection.remotePeer)
413
- return undefined
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
- if (response.dialResponse.status === Message.ResponseStatus.OK) {
417
- // make sure we use different network segments
418
- const options = connection.remoteAddr.toOptions()
419
- let segment: string
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
- if (networkSegments.includes(segment)) {
433
- this.log('already have response from network segment %d - %s', segment, options.host)
434
- return undefined
435
- }
676
+ results.verifyingPeers.add(connection.remotePeer)
677
+ results.networkSegments.push(segment)
436
678
 
437
- networkSegments.push(segment)
438
- }
679
+ if (status === Message.ResponseStatus.OK) {
680
+ results.success++
439
681
 
440
- return response.dialResponse
441
- } catch (err) {
442
- this.log.error('error asking remote to verify multiaddr', err)
443
- } finally {
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
- // find some random peers
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
- this.log('autonat response for %a is %s', addr, dialResponse.status)
693
+ if (results.success === REQUIRED_SUCCESSFUL_DIALS) {
694
+ this.confirmAddress(results)
695
+ }
463
696
 
464
- if (dialResponse.status === Message.ResponseStatus.E_BAD_REQUEST) {
465
- // the remote could not parse our request
466
- continue
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
- if (dialResponse.status === Message.ResponseStatus.E_DIAL_REFUSED) {
470
- // the remote could not honour our request
471
- continue
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
- if (dialResponse.addr == null && multiaddrs.length > 1) {
475
- // we sent the remote multiple addrs but they didn't tell us which ones worked/didn't work
476
- continue
477
- }
716
+ return ((currentConnectionCount / maxConnections) * 100) < this.connectionThreshold
717
+ }
478
718
 
479
- if (!multiaddrs.some(ma => ma.equals(addr))) {
480
- this.log('peer reported %a as %s but it was not in our observed address list', addr, dialResponse.status)
481
- continue
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
- const addrStr = addr.toString()
725
+ // abort & remove any outstanding verification jobs for this multiaddr
726
+ results.result = true
727
+ results.queue.abort()
728
+ }
485
729
 
486
- if (results[addrStr] == null) {
487
- results[addrStr] = { success: 0, failure: 0 }
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
- if (dialResponse.status === Message.ResponseStatus.OK) {
491
- results[addrStr].success++
492
- } else if (dialResponse.status === Message.ResponseStatus.E_DIAL_ERROR) {
493
- results[addrStr].failure++
494
- }
736
+ // abort & remove any outstanding verification jobs for this multiaddr
737
+ results.result = false
738
+ results.queue.abort()
739
+ }
495
740
 
496
- if (results[addrStr].success === REQUIRED_SUCCESSFUL_DIALS) {
497
- // we are now convinced
498
- this.log('%a is externally dialable', addr)
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
- if (results[addrStr].failure === REQUIRED_SUCCESSFUL_DIALS) {
504
- // we are now unconvinced
505
- this.log('%a is not externally dialable', addr)
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
  }