@libp2p/autonat 0.0.0-05b52d69c

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 ADDED
@@ -0,0 +1,525 @@
1
+ import { CodeError, ERR_TIMEOUT } from '@libp2p/interface/errors'
2
+ import { setMaxListeners } from '@libp2p/interface/events'
3
+ import { peerIdFromBytes } from '@libp2p/peer-id'
4
+ import { createEd25519PeerId } from '@libp2p/peer-id-factory'
5
+ import { multiaddr, protocols } from '@multiformats/multiaddr'
6
+ import first from 'it-first'
7
+ import * as lp from 'it-length-prefixed'
8
+ import map from 'it-map'
9
+ import parallel from 'it-parallel'
10
+ import { pipe } from 'it-pipe'
11
+ import isPrivateIp from 'private-ip'
12
+ import {
13
+ MAX_INBOUND_STREAMS,
14
+ MAX_OUTBOUND_STREAMS,
15
+ PROTOCOL_NAME, PROTOCOL_PREFIX, PROTOCOL_VERSION, REFRESH_INTERVAL, STARTUP_DELAY, TIMEOUT
16
+ } from './constants.js'
17
+ import { Message } from './pb/index.js'
18
+ import type { AutoNATComponents, AutoNATServiceInit } from './index.js'
19
+ import type { Logger } from '@libp2p/interface'
20
+ import type { Connection } from '@libp2p/interface/connection'
21
+ import type { PeerId } from '@libp2p/interface/peer-id'
22
+ import type { PeerInfo } from '@libp2p/interface/peer-info'
23
+ import type { Startable } from '@libp2p/interface/startable'
24
+ import type { IncomingStreamData } from '@libp2p/interface-internal/registrar'
25
+
26
+ // if more than 3 peers manage to dial us on what we believe to be our external
27
+ // IP then we are convinced that it is, in fact, our external IP
28
+ // https://github.com/libp2p/specs/blob/master/autonat/README.md#autonat-protocol
29
+ const REQUIRED_SUCCESSFUL_DIALS = 4
30
+
31
+ export class AutoNATService implements Startable {
32
+ private readonly components: AutoNATComponents
33
+ private readonly startupDelay: number
34
+ private readonly refreshInterval: number
35
+ private readonly protocol: string
36
+ private readonly timeout: number
37
+ private readonly maxInboundStreams: number
38
+ private readonly maxOutboundStreams: number
39
+ private verifyAddressTimeout?: ReturnType<typeof setTimeout>
40
+ private started: boolean
41
+ private readonly log: Logger
42
+
43
+ constructor (components: AutoNATComponents, init: AutoNATServiceInit) {
44
+ this.components = components
45
+ this.log = components.logger.forComponent('libp2p:autonat')
46
+ this.started = false
47
+ this.protocol = `/${init.protocolPrefix ?? PROTOCOL_PREFIX}/${PROTOCOL_NAME}/${PROTOCOL_VERSION}`
48
+ this.timeout = init.timeout ?? TIMEOUT
49
+ this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS
50
+ this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS
51
+ this.startupDelay = init.startupDelay ?? STARTUP_DELAY
52
+ this.refreshInterval = init.refreshInterval ?? REFRESH_INTERVAL
53
+ this._verifyExternalAddresses = this._verifyExternalAddresses.bind(this)
54
+ }
55
+
56
+ isStarted (): boolean {
57
+ return this.started
58
+ }
59
+
60
+ async start (): Promise<void> {
61
+ if (this.started) {
62
+ return
63
+ }
64
+
65
+ await this.components.registrar.handle(this.protocol, (data) => {
66
+ void this.handleIncomingAutonatStream(data)
67
+ .catch(err => {
68
+ this.log.error('error handling incoming autonat stream', err)
69
+ })
70
+ }, {
71
+ maxInboundStreams: this.maxInboundStreams,
72
+ maxOutboundStreams: this.maxOutboundStreams
73
+ })
74
+
75
+ this.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.startupDelay)
76
+
77
+ this.started = true
78
+ }
79
+
80
+ async stop (): Promise<void> {
81
+ await this.components.registrar.unhandle(this.protocol)
82
+ clearTimeout(this.verifyAddressTimeout)
83
+
84
+ this.started = false
85
+ }
86
+
87
+ /**
88
+ * Handle an incoming AutoNAT request
89
+ */
90
+ async handleIncomingAutonatStream (data: IncomingStreamData): Promise<void> {
91
+ const signal = AbortSignal.timeout(this.timeout)
92
+
93
+ const onAbort = (): void => {
94
+ data.stream.abort(new CodeError('handleIncomingAutonatStream timeout', ERR_TIMEOUT))
95
+ }
96
+
97
+ signal.addEventListener('abort', onAbort, { once: true })
98
+
99
+ // this controller may be used while dialing lots of peers so prevent MaxListenersExceededWarning
100
+ // appearing in the console
101
+ setMaxListeners(Infinity, signal)
102
+
103
+ const ourHosts = this.components.addressManager.getAddresses()
104
+ .map(ma => ma.toOptions().host)
105
+
106
+ try {
107
+ const self = this
108
+
109
+ await pipe(
110
+ data.stream,
111
+ (source) => lp.decode(source),
112
+ async function * (stream) {
113
+ const buf = await first(stream)
114
+
115
+ if (buf == null) {
116
+ self.log('no message received')
117
+ yield Message.encode({
118
+ type: Message.MessageType.DIAL_RESPONSE,
119
+ dialResponse: {
120
+ status: Message.ResponseStatus.E_BAD_REQUEST,
121
+ statusText: 'No message was sent'
122
+ }
123
+ })
124
+
125
+ return
126
+ }
127
+
128
+ let request: Message
129
+
130
+ try {
131
+ request = Message.decode(buf)
132
+ } catch (err) {
133
+ self.log.error('could not decode message', err)
134
+
135
+ yield Message.encode({
136
+ type: Message.MessageType.DIAL_RESPONSE,
137
+ dialResponse: {
138
+ status: Message.ResponseStatus.E_BAD_REQUEST,
139
+ statusText: 'Could not decode message'
140
+ }
141
+ })
142
+
143
+ return
144
+ }
145
+
146
+ const dialRequest = request.dial
147
+
148
+ if (dialRequest == null) {
149
+ self.log.error('dial was missing from message')
150
+
151
+ yield Message.encode({
152
+ type: Message.MessageType.DIAL_RESPONSE,
153
+ dialResponse: {
154
+ status: Message.ResponseStatus.E_BAD_REQUEST,
155
+ statusText: 'No Dial message found in message'
156
+ }
157
+ })
158
+
159
+ return
160
+ }
161
+
162
+ let peerId: PeerId
163
+ const peer = dialRequest.peer
164
+
165
+ if (peer == null || peer.id == null) {
166
+ self.log.error('PeerId missing from message')
167
+
168
+ yield Message.encode({
169
+ type: Message.MessageType.DIAL_RESPONSE,
170
+ dialResponse: {
171
+ status: Message.ResponseStatus.E_BAD_REQUEST,
172
+ statusText: 'missing peer info'
173
+ }
174
+ })
175
+
176
+ return
177
+ }
178
+
179
+ try {
180
+ peerId = peerIdFromBytes(peer.id)
181
+ } catch (err) {
182
+ self.log.error('invalid PeerId', err)
183
+
184
+ yield Message.encode({
185
+ type: Message.MessageType.DIAL_RESPONSE,
186
+ dialResponse: {
187
+ status: Message.ResponseStatus.E_BAD_REQUEST,
188
+ statusText: 'bad peer id'
189
+ }
190
+ })
191
+
192
+ return
193
+ }
194
+
195
+ self.log('incoming request from %p', peerId)
196
+
197
+ // reject any dial requests that arrive via relays
198
+ if (!data.connection.remotePeer.equals(peerId)) {
199
+ self.log('target peer %p did not equal sending peer %p', peerId, data.connection.remotePeer)
200
+
201
+ yield Message.encode({
202
+ type: Message.MessageType.DIAL_RESPONSE,
203
+ dialResponse: {
204
+ status: Message.ResponseStatus.E_BAD_REQUEST,
205
+ statusText: 'peer id mismatch'
206
+ }
207
+ })
208
+
209
+ return
210
+ }
211
+
212
+ // get a list of multiaddrs to dial
213
+ const multiaddrs = peer.addrs
214
+ .map(buf => multiaddr(buf))
215
+ .filter(ma => {
216
+ const isFromSameHost = ma.toOptions().host === data.connection.remoteAddr.toOptions().host
217
+
218
+ self.log.trace('request to dial %a was sent from %a is same host %s', ma, data.connection.remoteAddr, isFromSameHost)
219
+ // skip any Multiaddrs where the target node's IP does not match the sending node's IP
220
+ return isFromSameHost
221
+ })
222
+ .filter(ma => {
223
+ const host = ma.toOptions().host
224
+ const isPublicIp = !(isPrivateIp(host) ?? false)
225
+
226
+ self.log.trace('host %s was public %s', host, isPublicIp)
227
+ // don't try to dial private addresses
228
+ return isPublicIp
229
+ })
230
+ .filter(ma => {
231
+ const host = ma.toOptions().host
232
+ const isNotOurHost = !ourHosts.includes(host)
233
+
234
+ self.log.trace('host %s was not our host %s', host, isNotOurHost)
235
+ // don't try to dial nodes on the same host as us
236
+ return isNotOurHost
237
+ })
238
+ .filter(ma => {
239
+ const isSupportedTransport = Boolean(self.components.transportManager.transportForMultiaddr(ma))
240
+
241
+ self.log.trace('transport for %a is supported %s', ma, isSupportedTransport)
242
+ // skip any Multiaddrs that have transports we do not support
243
+ return isSupportedTransport
244
+ })
245
+ .map(ma => {
246
+ if (ma.getPeerId() == null) {
247
+ // make sure we have the PeerId as part of the Multiaddr
248
+ ma = ma.encapsulate(`/p2p/${peerId.toString()}`)
249
+ }
250
+
251
+ return ma
252
+ })
253
+
254
+ // make sure we have something to dial
255
+ if (multiaddrs.length === 0) {
256
+ self.log('no valid multiaddrs for %p in message', peerId)
257
+
258
+ yield Message.encode({
259
+ type: Message.MessageType.DIAL_RESPONSE,
260
+ dialResponse: {
261
+ status: Message.ResponseStatus.E_DIAL_REFUSED,
262
+ statusText: 'no dialable addresses'
263
+ }
264
+ })
265
+
266
+ return
267
+ }
268
+
269
+ self.log('dial multiaddrs %s for peer %p', multiaddrs.map(ma => ma.toString()).join(', '), peerId)
270
+
271
+ let errorMessage = ''
272
+ let lastMultiaddr = multiaddrs[0]
273
+
274
+ for await (const multiaddr of multiaddrs) {
275
+ let connection: Connection | undefined
276
+ lastMultiaddr = multiaddr
277
+
278
+ try {
279
+ connection = await self.components.connectionManager.openConnection(multiaddr, {
280
+ signal
281
+ })
282
+
283
+ if (!connection.remoteAddr.equals(multiaddr)) {
284
+ self.log.error('tried to dial %a but dialed %a', multiaddr, connection.remoteAddr)
285
+ throw new Error('Unexpected remote address')
286
+ }
287
+
288
+ self.log('Success %p', peerId)
289
+
290
+ yield Message.encode({
291
+ type: Message.MessageType.DIAL_RESPONSE,
292
+ dialResponse: {
293
+ status: Message.ResponseStatus.OK,
294
+ addr: connection.remoteAddr.decapsulateCode(protocols('p2p').code).bytes
295
+ }
296
+ })
297
+
298
+ return
299
+ } catch (err: any) {
300
+ self.log('could not dial %p', peerId, err)
301
+ errorMessage = err.message
302
+ } finally {
303
+ if (connection != null) {
304
+ await connection.close()
305
+ }
306
+ }
307
+ }
308
+
309
+ yield Message.encode({
310
+ type: Message.MessageType.DIAL_RESPONSE,
311
+ dialResponse: {
312
+ status: Message.ResponseStatus.E_DIAL_ERROR,
313
+ statusText: errorMessage,
314
+ addr: lastMultiaddr.bytes
315
+ }
316
+ })
317
+ },
318
+ (source) => lp.encode(source),
319
+ data.stream
320
+ )
321
+ } catch (err) {
322
+ this.log.error('error handling incoming autonat stream', err)
323
+ } finally {
324
+ signal.removeEventListener('abort', onAbort)
325
+ }
326
+ }
327
+
328
+ _verifyExternalAddresses (): void {
329
+ void this.verifyExternalAddresses()
330
+ .catch(err => {
331
+ this.log.error('error verifying external address', err)
332
+ })
333
+ }
334
+
335
+ /**
336
+ * Our multicodec topology noticed a new peer that supports autonat
337
+ */
338
+ async verifyExternalAddresses (): Promise<void> {
339
+ clearTimeout(this.verifyAddressTimeout)
340
+
341
+ // Do not try to push if we are not running
342
+ if (!this.isStarted()) {
343
+ return
344
+ }
345
+
346
+ const addressManager = this.components.addressManager
347
+
348
+ const multiaddrs = addressManager.getObservedAddrs()
349
+ .filter(ma => {
350
+ const options = ma.toOptions()
351
+
352
+ return !(isPrivateIp(options.host) ?? false)
353
+ })
354
+
355
+ if (multiaddrs.length === 0) {
356
+ this.log('no public addresses found, not requesting verification')
357
+ this.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.refreshInterval)
358
+
359
+ return
360
+ }
361
+
362
+ const signal = AbortSignal.timeout(this.timeout)
363
+
364
+ // this controller may be used while dialing lots of peers so prevent MaxListenersExceededWarning
365
+ // appearing in the console
366
+ setMaxListeners(Infinity, signal)
367
+
368
+ const self = this
369
+
370
+ try {
371
+ this.log('verify multiaddrs %s', multiaddrs.map(ma => ma.toString()).join(', '))
372
+
373
+ const request = Message.encode({
374
+ type: Message.MessageType.DIAL,
375
+ dial: {
376
+ peer: {
377
+ id: this.components.peerId.toBytes(),
378
+ addrs: multiaddrs.map(map => map.bytes)
379
+ }
380
+ }
381
+ })
382
+ // find some random peers
383
+ const randomPeer = await createEd25519PeerId()
384
+ const randomCid = randomPeer.toBytes()
385
+
386
+ const results: Record<string, { success: number, failure: number }> = {}
387
+ const networkSegments: string[] = []
388
+
389
+ const verifyAddress = async (peer: PeerInfo): Promise<Message.DialResponse | undefined> => {
390
+ let onAbort = (): void => {}
391
+
392
+ try {
393
+ this.log('asking %p to verify multiaddr', peer.id)
394
+
395
+ const connection = await self.components.connectionManager.openConnection(peer.id, {
396
+ signal
397
+ })
398
+
399
+ const stream = await connection.newStream(this.protocol, {
400
+ signal
401
+ })
402
+
403
+ onAbort = () => { stream.abort(new CodeError('verifyAddress timeout', ERR_TIMEOUT)) }
404
+
405
+ signal.addEventListener('abort', onAbort, { once: true })
406
+
407
+ const buf = await pipe(
408
+ [request],
409
+ (source) => lp.encode(source),
410
+ stream,
411
+ (source) => lp.decode(source),
412
+ async (stream) => first(stream)
413
+ )
414
+ if (buf == null) {
415
+ this.log('no response received from %p', connection.remotePeer)
416
+ return undefined
417
+ }
418
+ const response = Message.decode(buf)
419
+
420
+ if (response.type !== Message.MessageType.DIAL_RESPONSE || response.dialResponse == null) {
421
+ this.log('invalid autonat response from %p', connection.remotePeer)
422
+ return undefined
423
+ }
424
+
425
+ if (response.dialResponse.status === Message.ResponseStatus.OK) {
426
+ // make sure we use different network segments
427
+ const options = connection.remoteAddr.toOptions()
428
+ let segment: string
429
+
430
+ if (options.family === 4) {
431
+ const octets = options.host.split('.')
432
+ segment = octets[0]
433
+ } else if (options.family === 6) {
434
+ const octets = options.host.split(':')
435
+ segment = octets[0]
436
+ } else {
437
+ this.log('remote address "%s" was not IP4 or IP6?', options.host)
438
+ return undefined
439
+ }
440
+
441
+ if (networkSegments.includes(segment)) {
442
+ this.log('already have response from network segment %d - %s', segment, options.host)
443
+ return undefined
444
+ }
445
+
446
+ networkSegments.push(segment)
447
+ }
448
+
449
+ return response.dialResponse
450
+ } catch (err) {
451
+ this.log.error('error asking remote to verify multiaddr', err)
452
+ } finally {
453
+ signal.removeEventListener('abort', onAbort)
454
+ }
455
+ }
456
+
457
+ for await (const dialResponse of parallel(map(this.components.peerRouting.getClosestPeers(randomCid, {
458
+ signal
459
+ }), (peer) => async () => verifyAddress(peer)), {
460
+ concurrency: REQUIRED_SUCCESSFUL_DIALS
461
+ })) {
462
+ try {
463
+ if (dialResponse == null) {
464
+ continue
465
+ }
466
+
467
+ // they either told us which address worked/didn't work, or we only sent them one address
468
+ const addr = dialResponse.addr == null ? multiaddrs[0] : multiaddr(dialResponse.addr)
469
+
470
+ this.log('autonat response for %a is %s', addr, dialResponse.status)
471
+
472
+ if (dialResponse.status === Message.ResponseStatus.E_BAD_REQUEST) {
473
+ // the remote could not parse our request
474
+ continue
475
+ }
476
+
477
+ if (dialResponse.status === Message.ResponseStatus.E_DIAL_REFUSED) {
478
+ // the remote could not honour our request
479
+ continue
480
+ }
481
+
482
+ if (dialResponse.addr == null && multiaddrs.length > 1) {
483
+ // we sent the remote multiple addrs but they didn't tell us which ones worked/didn't work
484
+ continue
485
+ }
486
+
487
+ if (!multiaddrs.some(ma => ma.equals(addr))) {
488
+ this.log('peer reported %a as %s but it was not in our observed address list', addr, dialResponse.status)
489
+ continue
490
+ }
491
+
492
+ const addrStr = addr.toString()
493
+
494
+ if (results[addrStr] == null) {
495
+ results[addrStr] = { success: 0, failure: 0 }
496
+ }
497
+
498
+ if (dialResponse.status === Message.ResponseStatus.OK) {
499
+ results[addrStr].success++
500
+ } else if (dialResponse.status === Message.ResponseStatus.E_DIAL_ERROR) {
501
+ results[addrStr].failure++
502
+ }
503
+
504
+ if (results[addrStr].success === REQUIRED_SUCCESSFUL_DIALS) {
505
+ // we are now convinced
506
+ this.log('%a is externally dialable', addr)
507
+ addressManager.confirmObservedAddr(addr)
508
+ return
509
+ }
510
+
511
+ if (results[addrStr].failure === REQUIRED_SUCCESSFUL_DIALS) {
512
+ // we are now unconvinced
513
+ this.log('%a is not externally dialable', addr)
514
+ addressManager.removeObservedAddr(addr)
515
+ return
516
+ }
517
+ } catch (err) {
518
+ this.log.error('could not verify external address', err)
519
+ }
520
+ }
521
+ } finally {
522
+ this.verifyAddressTimeout = setTimeout(this._verifyExternalAddresses, this.refreshInterval)
523
+ }
524
+ }
525
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The prefix to use in the protocol
3
+ */
4
+ export const PROTOCOL_PREFIX = 'libp2p'
5
+
6
+ /**
7
+ * The name to use in the protocol
8
+ */
9
+ export const PROTOCOL_NAME = 'autonat'
10
+
11
+ /**
12
+ * The version to use in the protocol
13
+ */
14
+ export const PROTOCOL_VERSION = '1.0.0'
15
+ export const TIMEOUT = 30000
16
+ export const STARTUP_DELAY = 5000
17
+ export const REFRESH_INTERVAL = 60000
18
+ export const MAX_INBOUND_STREAMS = 1
19
+ export const MAX_OUTBOUND_STREAMS = 1
package/src/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Use the `autoNATService` function to add support for the [AutoNAT protocol](https://docs.libp2p.io/concepts/nat/autonat/)
5
+ * to libp2p.
6
+ *
7
+ * @example
8
+ *
9
+ * ```typescript
10
+ * import { createLibp2p } from 'libp2p'
11
+ * import { autoNAT } from '@libp2p/autonat'
12
+ *
13
+ * const node = await createLibp2p({
14
+ * // ...other options
15
+ * services: {
16
+ * autoNAT: autoNAT()
17
+ * }
18
+ * })
19
+ * ```
20
+ */
21
+
22
+ import { AutoNATService } from './autonat.js'
23
+ import type { ComponentLogger } from '@libp2p/interface'
24
+ import type { PeerId } from '@libp2p/interface/peer-id'
25
+ import type { PeerRouting } from '@libp2p/interface/peer-routing'
26
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager'
27
+ import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
28
+ import type { Registrar } from '@libp2p/interface-internal/registrar'
29
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
30
+
31
+ export interface AutoNATServiceInit {
32
+ /**
33
+ * Allows overriding the protocol prefix used
34
+ */
35
+ protocolPrefix?: string
36
+
37
+ /**
38
+ * How long we should wait for a remote peer to verify our external address
39
+ */
40
+ timeout?: number
41
+
42
+ /**
43
+ * How long to wait after startup before trying to verify our external address
44
+ */
45
+ startupDelay?: number
46
+
47
+ /**
48
+ * Verify our external addresses this often
49
+ */
50
+ refreshInterval?: number
51
+
52
+ /**
53
+ * How many parallel inbound autoNAT streams we allow per-connection
54
+ */
55
+ maxInboundStreams?: number
56
+
57
+ /**
58
+ * How many parallel outbound autoNAT streams we allow per-connection
59
+ */
60
+ maxOutboundStreams?: number
61
+ }
62
+
63
+ export interface AutoNATComponents {
64
+ registrar: Registrar
65
+ addressManager: AddressManager
66
+ transportManager: TransportManager
67
+ peerId: PeerId
68
+ connectionManager: ConnectionManager
69
+ peerRouting: PeerRouting
70
+ logger: ComponentLogger
71
+ }
72
+
73
+ export function autoNAT (init: AutoNATServiceInit = {}): (components: AutoNATComponents) => unknown {
74
+ return (components) => {
75
+ return new AutoNATService(components, init)
76
+ }
77
+ }
@@ -0,0 +1,35 @@
1
+ syntax = "proto3";
2
+
3
+ message Message {
4
+ enum MessageType {
5
+ DIAL = 0;
6
+ DIAL_RESPONSE = 1;
7
+ }
8
+
9
+ enum ResponseStatus {
10
+ OK = 0;
11
+ E_DIAL_ERROR = 100;
12
+ E_DIAL_REFUSED = 101;
13
+ E_BAD_REQUEST = 200;
14
+ E_INTERNAL_ERROR = 300;
15
+ }
16
+
17
+ message PeerInfo {
18
+ optional bytes id = 1;
19
+ repeated bytes addrs = 2;
20
+ }
21
+
22
+ message Dial {
23
+ optional PeerInfo peer = 1;
24
+ }
25
+
26
+ message DialResponse {
27
+ optional ResponseStatus status = 1;
28
+ optional string statusText = 2;
29
+ optional bytes addr = 3;
30
+ }
31
+
32
+ optional MessageType type = 1;
33
+ optional Dial dial = 2;
34
+ optional DialResponse dialResponse = 3;
35
+ }