@libp2p/autonat-v2 0.0.0-2d6079bc1
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 +74 -0
- package/dist/index.min.js +19 -0
- package/dist/index.min.js.map +7 -0
- package/dist/src/autonat.d.ts +14 -0
- package/dist/src/autonat.d.ts.map +1 -0
- package/dist/src/autonat.js +38 -0
- package/dist/src/autonat.js.map +1 -0
- package/dist/src/client.d.ts +58 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +476 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/constants.d.ts +22 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +22 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +93 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +30 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/pb/index.d.ts +90 -0
- package/dist/src/pb/index.d.ts.map +1 -0
- package/dist/src/pb/index.js +488 -0
- package/dist/src/pb/index.js.map +1 -0
- package/dist/src/server.d.ts +27 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +191 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +4 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +75 -0
- package/src/autonat.ts +47 -0
- package/src/client.ts +628 -0
- package/src/constants.ts +24 -0
- package/src/index.ts +110 -0
- package/src/pb/index.proto +56 -0
- package/src/pb/index.ts +614 -0
- package/src/server.ts +237 -0
- package/src/utils.ts +3 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { ProtocolError, serviceCapabilities, serviceDependencies } from '@libp2p/interface'
|
|
2
|
+
import { peerSet } from '@libp2p/peer-collections'
|
|
3
|
+
import { createScalableCuckooFilter } from '@libp2p/utils/filters'
|
|
4
|
+
import { isGlobalUnicast } from '@libp2p/utils/multiaddr/is-global-unicast'
|
|
5
|
+
import { isPrivate } from '@libp2p/utils/multiaddr/is-private'
|
|
6
|
+
import { PeerQueue } from '@libp2p/utils/peer-queue'
|
|
7
|
+
import { repeatingTask } from '@libp2p/utils/repeating-task'
|
|
8
|
+
import { trackedMap } from '@libp2p/utils/tracked-map'
|
|
9
|
+
import { anySignal } from 'any-signal'
|
|
10
|
+
import { pbStream } from 'it-protobuf-stream'
|
|
11
|
+
import { setMaxListeners } from 'main-event'
|
|
12
|
+
import { DEFAULT_CONNECTION_THRESHOLD, DIAL_DATA_CHUNK_SIZE, MAX_DIAL_DATA_BYTES, MAX_INBOUND_STREAMS, MAX_MESSAGE_SIZE, MAX_OUTBOUND_STREAMS, TIMEOUT } from './constants.ts'
|
|
13
|
+
import { DialBack, DialBackResponse, DialResponse, DialStatus, Message } from './pb/index.ts'
|
|
14
|
+
import { randomNumber } from './utils.ts'
|
|
15
|
+
import type { AutoNATv2Components, AutoNATv2ServiceInit } from './index.ts'
|
|
16
|
+
import type { Logger, Connection, Startable, AbortOptions, IncomingStreamData } from '@libp2p/interface'
|
|
17
|
+
import type { AddressType } from '@libp2p/interface-internal'
|
|
18
|
+
import type { PeerSet } from '@libp2p/peer-collections'
|
|
19
|
+
import type { Filter } from '@libp2p/utils/filters'
|
|
20
|
+
import type { RepeatingTask } from '@libp2p/utils/repeating-task'
|
|
21
|
+
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
22
|
+
|
|
23
|
+
// if more than 3 peers manage to dial us on what we believe to be our external
|
|
24
|
+
// IP then we are convinced that it is, in fact, our external IP
|
|
25
|
+
// https://github.com/libp2p/specs/blob/master/autonat/autonat-v1.md#autonat-protocol
|
|
26
|
+
const REQUIRED_SUCCESSFUL_DIALS = 4
|
|
27
|
+
const REQUIRED_FAILED_DIALS = 8
|
|
28
|
+
|
|
29
|
+
interface DialResults {
|
|
30
|
+
/**
|
|
31
|
+
* The address being tested
|
|
32
|
+
*/
|
|
33
|
+
multiaddr: Multiaddr
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The number of successful dials from peers
|
|
37
|
+
*/
|
|
38
|
+
success: number
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The number of dial failures from peers
|
|
42
|
+
*/
|
|
43
|
+
failure: number
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* For the multiaddr corresponding the the string key of the `dialResults`
|
|
47
|
+
* map, these are the IP segments that a successful dial result has been
|
|
48
|
+
* received from
|
|
49
|
+
*/
|
|
50
|
+
networkSegments: string[]
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ensure that the same peer id can't verify multiple times
|
|
54
|
+
*/
|
|
55
|
+
verifyingPeers: PeerSet
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Updated when this address is verified or failed
|
|
59
|
+
*/
|
|
60
|
+
result?: boolean
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The type of address
|
|
64
|
+
*/
|
|
65
|
+
type: AddressType
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The last time the address was verified
|
|
69
|
+
*/
|
|
70
|
+
lastVerified?: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AutoNATv2ClientInit extends AutoNATv2ServiceInit {
|
|
74
|
+
dialRequestProtocol: string
|
|
75
|
+
dialBackProtocol: string
|
|
76
|
+
maxDialDataBytes?: bigint
|
|
77
|
+
dialDataChunkSize?: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class AutoNATv2Client implements Startable {
|
|
81
|
+
private readonly components: AutoNATv2Components
|
|
82
|
+
private readonly dialRequestProtocol: string
|
|
83
|
+
private readonly dialBackProtocol: string
|
|
84
|
+
private readonly timeout: number
|
|
85
|
+
private readonly maxInboundStreams: number
|
|
86
|
+
private readonly maxOutboundStreams: number
|
|
87
|
+
private readonly maxMessageSize: number
|
|
88
|
+
private readonly maxDialDataBytes: bigint
|
|
89
|
+
private readonly dialDataChunkSize: number
|
|
90
|
+
private started: boolean
|
|
91
|
+
private readonly log: Logger
|
|
92
|
+
private topologyId?: string
|
|
93
|
+
private readonly dialResults: Map<string, DialResults>
|
|
94
|
+
private readonly findPeers: RepeatingTask
|
|
95
|
+
private readonly addressFilter: Filter
|
|
96
|
+
private readonly connectionThreshold: number
|
|
97
|
+
private readonly queue: PeerQueue
|
|
98
|
+
private readonly nonces: Set<bigint>
|
|
99
|
+
|
|
100
|
+
constructor (components: AutoNATv2Components, init: AutoNATv2ClientInit) {
|
|
101
|
+
this.components = components
|
|
102
|
+
this.log = components.logger.forComponent('libp2p:auto-nat-v2:client')
|
|
103
|
+
this.started = false
|
|
104
|
+
this.dialRequestProtocol = init.dialRequestProtocol
|
|
105
|
+
this.dialBackProtocol = init.dialBackProtocol
|
|
106
|
+
this.timeout = init.timeout ?? TIMEOUT
|
|
107
|
+
this.maxInboundStreams = init.maxInboundStreams ?? MAX_INBOUND_STREAMS
|
|
108
|
+
this.maxOutboundStreams = init.maxOutboundStreams ?? MAX_OUTBOUND_STREAMS
|
|
109
|
+
this.connectionThreshold = init.connectionThreshold ?? DEFAULT_CONNECTION_THRESHOLD
|
|
110
|
+
this.maxMessageSize = init.maxMessageSize ?? MAX_MESSAGE_SIZE
|
|
111
|
+
this.dialResults = trackedMap({
|
|
112
|
+
name: 'libp2p_autonat_v2_dial_results',
|
|
113
|
+
metrics: components.metrics
|
|
114
|
+
})
|
|
115
|
+
this.findPeers = repeatingTask(this.findRandomPeers.bind(this), 60_000)
|
|
116
|
+
this.addressFilter = createScalableCuckooFilter(1024)
|
|
117
|
+
this.queue = new PeerQueue({
|
|
118
|
+
concurrency: 3,
|
|
119
|
+
maxSize: 50
|
|
120
|
+
})
|
|
121
|
+
this.maxDialDataBytes = init.maxDialDataBytes ?? MAX_DIAL_DATA_BYTES
|
|
122
|
+
this.dialDataChunkSize = init.dialDataChunkSize ?? DIAL_DATA_CHUNK_SIZE
|
|
123
|
+
|
|
124
|
+
this.nonces = new Set()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
readonly [Symbol.toStringTag] = '@libp2p/autonat-v2'
|
|
128
|
+
|
|
129
|
+
readonly [serviceCapabilities]: string[] = [
|
|
130
|
+
'@libp2p/autonat'
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
get [serviceDependencies] (): string[] {
|
|
134
|
+
return [
|
|
135
|
+
'@libp2p/identify'
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isStarted (): boolean {
|
|
140
|
+
return this.started
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async start (): Promise<void> {
|
|
144
|
+
if (this.started) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.topologyId = await this.components.registrar.register(this.dialRequestProtocol, {
|
|
149
|
+
onConnect: (peerId, connection) => {
|
|
150
|
+
this.verifyExternalAddresses(connection)
|
|
151
|
+
.catch(err => {
|
|
152
|
+
this.log.error('could not verify addresses - %e', err)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
await this.components.registrar.handle(this.dialBackProtocol, (data) => {
|
|
158
|
+
void this.handleDialBackStream(data)
|
|
159
|
+
.catch(err => {
|
|
160
|
+
this.log.error('error handling incoming autonat stream - %e', err)
|
|
161
|
+
})
|
|
162
|
+
}, {
|
|
163
|
+
maxInboundStreams: this.maxInboundStreams,
|
|
164
|
+
maxOutboundStreams: this.maxOutboundStreams
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
this.findPeers.start()
|
|
168
|
+
this.started = true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async stop (): Promise<void> {
|
|
172
|
+
await this.components.registrar.unhandle(this.dialRequestProtocol)
|
|
173
|
+
await this.components.registrar.unhandle(this.dialBackProtocol)
|
|
174
|
+
|
|
175
|
+
if (this.topologyId != null) {
|
|
176
|
+
await this.components.registrar.unhandle(this.topologyId)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.dialResults.clear()
|
|
180
|
+
this.findPeers.stop()
|
|
181
|
+
this.started = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private allAddressesAreVerified (): boolean {
|
|
185
|
+
return this.components.addressManager.getAddressesWithMetadata().every(addr => {
|
|
186
|
+
if (addr.expires > Date.now()) {
|
|
187
|
+
// ignore any unverified addresses within their TTL
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return addr.verified
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async findRandomPeers (options?: AbortOptions): Promise<void> {
|
|
196
|
+
// skip if all addresses are verified
|
|
197
|
+
if (this.allAddressesAreVerified()) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const signal = anySignal([
|
|
202
|
+
AbortSignal.timeout(10_000),
|
|
203
|
+
options?.signal
|
|
204
|
+
])
|
|
205
|
+
|
|
206
|
+
// spend a few seconds finding random peers - dial them which will run
|
|
207
|
+
// identify to trigger the topology callbacks and run AutoNAT
|
|
208
|
+
try {
|
|
209
|
+
this.log('starting random walk to find peers to run AutoNAT')
|
|
210
|
+
|
|
211
|
+
for await (const peer of this.components.randomWalk.walk({ signal })) {
|
|
212
|
+
if (!(await this.components.connectionManager.isDialable(peer.multiaddrs))) {
|
|
213
|
+
this.log.trace('random peer %p was not dialable %s', peer.id, peer.multiaddrs.map(ma => ma.toString()).join(', '))
|
|
214
|
+
|
|
215
|
+
// skip peers we can't dial
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
this.log.trace('dial random peer %p', peer.id)
|
|
221
|
+
await this.components.connectionManager.openConnection(peer.multiaddrs, {
|
|
222
|
+
signal
|
|
223
|
+
})
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
226
|
+
if (this.allAddressesAreVerified()) {
|
|
227
|
+
this.log('stopping random walk, all addresses are verified')
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!this.hasConnectionCapacity()) {
|
|
232
|
+
this.log('stopping random walk, too close to max connections')
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Handle an incoming AutoNAT request
|
|
241
|
+
*/
|
|
242
|
+
async handleDialBackStream (data: IncomingStreamData): Promise<void> {
|
|
243
|
+
const signal = AbortSignal.timeout(this.timeout)
|
|
244
|
+
setMaxListeners(Infinity, signal)
|
|
245
|
+
|
|
246
|
+
const messages = pbStream(data.stream, {
|
|
247
|
+
maxDataLength: this.maxMessageSize
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const message = await messages.read(DialBack, {
|
|
252
|
+
signal
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
// TODO: need to verify that the incoming address is the one we asked the
|
|
256
|
+
// peer to dial us on
|
|
257
|
+
if (!this.nonces.has(message.nonce)) {
|
|
258
|
+
throw new ProtocolError('No matching dial found for nonce value')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.nonces.delete(message.nonce)
|
|
262
|
+
|
|
263
|
+
await messages.write({
|
|
264
|
+
status: DialBackResponse.DialBackStatus.OK
|
|
265
|
+
}, DialBackResponse)
|
|
266
|
+
|
|
267
|
+
await data.stream.close({
|
|
268
|
+
signal
|
|
269
|
+
})
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
this.log.error('error handling incoming dial back stream - %e', err)
|
|
272
|
+
data.stream.abort(err)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private getUnverifiedMultiaddrs (segment: string, supportsIPv6: boolean): DialResults[] {
|
|
277
|
+
const addrs = this.components.addressManager.getAddressesWithMetadata()
|
|
278
|
+
.sort((a, b) => {
|
|
279
|
+
// sort addresses, de-prioritize observed addresses
|
|
280
|
+
if (a.type === 'observed' && b.type !== 'observed') {
|
|
281
|
+
return 1
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (b.type === 'observed' && a.type !== 'observed') {
|
|
285
|
+
return -1
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return 0
|
|
289
|
+
})
|
|
290
|
+
.filter(addr => {
|
|
291
|
+
const expired = addr.expires < Date.now()
|
|
292
|
+
|
|
293
|
+
if (!expired) {
|
|
294
|
+
// skip verified/non-verified addresses within their TTL
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const options = addr.multiaddr.toOptions()
|
|
299
|
+
|
|
300
|
+
if (options.family === 6) {
|
|
301
|
+
// do not send IPv6 addresses to peers without IPv6 addresses
|
|
302
|
+
if (!supportsIPv6) {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!isGlobalUnicast(addr.multiaddr)) {
|
|
307
|
+
// skip non-globally routable addresses
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (isPrivate(addr.multiaddr)) {
|
|
313
|
+
// skip private addresses
|
|
314
|
+
return false
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return true
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const output: DialResults[] = []
|
|
321
|
+
|
|
322
|
+
for (const addr of addrs) {
|
|
323
|
+
const addrString = addr.multiaddr.toString()
|
|
324
|
+
let results = this.dialResults.get(addrString)
|
|
325
|
+
|
|
326
|
+
if (results != null) {
|
|
327
|
+
if (results.networkSegments.includes(segment)) {
|
|
328
|
+
this.log.trace('%a already has a network segment result from %s', results.multiaddr, segment)
|
|
329
|
+
// skip this address if we already have a dial result from the
|
|
330
|
+
// network segment the peer is in
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// will include this multiaddr, ensure we have a results object
|
|
336
|
+
if (results == null) {
|
|
337
|
+
const needsRevalidating = addr.expires < Date.now()
|
|
338
|
+
|
|
339
|
+
// allow re-validating addresses that worked previously
|
|
340
|
+
if (needsRevalidating) {
|
|
341
|
+
this.addressFilter.remove?.(addrString)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (this.addressFilter.has(addrString)) {
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// only try to validate the address once
|
|
349
|
+
this.addressFilter.add(addrString)
|
|
350
|
+
|
|
351
|
+
this.log.trace('creating dial result %s %s', needsRevalidating ? 'to revalidate' : 'for', addrString)
|
|
352
|
+
results = {
|
|
353
|
+
multiaddr: addr.multiaddr,
|
|
354
|
+
success: 0,
|
|
355
|
+
failure: 0,
|
|
356
|
+
networkSegments: [],
|
|
357
|
+
verifyingPeers: peerSet(),
|
|
358
|
+
type: addr.type,
|
|
359
|
+
lastVerified: addr.lastVerified
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
this.dialResults.set(addrString, results)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
output.push(results)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return output
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Removes any multiaddr result objects created for old multiaddrs that we are
|
|
373
|
+
* no longer waiting on
|
|
374
|
+
*/
|
|
375
|
+
private removeOutdatedMultiaddrResults (): void {
|
|
376
|
+
const unverifiedMultiaddrs = new Set(this.components.addressManager.getAddressesWithMetadata()
|
|
377
|
+
.filter(({ expires }) => {
|
|
378
|
+
if (expires < Date.now()) {
|
|
379
|
+
return true
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return false
|
|
383
|
+
})
|
|
384
|
+
.map(({ multiaddr }) => multiaddr.toString())
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
for (const multiaddr of this.dialResults.keys()) {
|
|
388
|
+
if (!unverifiedMultiaddrs.has(multiaddr)) {
|
|
389
|
+
this.log.trace('remove results for %a', multiaddr)
|
|
390
|
+
this.dialResults.delete(multiaddr)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Our multicodec topology noticed a new peer that supports autonat
|
|
397
|
+
*/
|
|
398
|
+
async verifyExternalAddresses (connection: Connection): Promise<void> {
|
|
399
|
+
// do nothing if we are not running
|
|
400
|
+
if (!this.isStarted()) {
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// perform cleanup
|
|
405
|
+
this.removeOutdatedMultiaddrResults()
|
|
406
|
+
|
|
407
|
+
const peer = await this.components.peerStore.get(connection.remotePeer)
|
|
408
|
+
|
|
409
|
+
// if the remote peer has IPv6 addresses, we can probably send them an IPv6
|
|
410
|
+
// address to verify, otherwise only send them IPv4 addresses
|
|
411
|
+
const supportsIPv6 = peer.addresses.some(({ multiaddr }) => {
|
|
412
|
+
return multiaddr.toOptions().family === 6
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
// get multiaddrs this peer is eligible to verify
|
|
416
|
+
const segment = this.getNetworkSegment(connection.remoteAddr)
|
|
417
|
+
const results = this.getUnverifiedMultiaddrs(segment, supportsIPv6)
|
|
418
|
+
|
|
419
|
+
if (results.length === 0) {
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!this.hasConnectionCapacity()) {
|
|
424
|
+
// we are near the max connection limit - any dial attempts from remote
|
|
425
|
+
// peers may be rejected which will get flagged as false dial errors and
|
|
426
|
+
// lead us to un-verify an otherwise reachable address
|
|
427
|
+
|
|
428
|
+
if (results[0]?.lastVerified != null) {
|
|
429
|
+
this.log('automatically re-verifying %a because we are too close to the connection limit', results[0].multiaddr)
|
|
430
|
+
this.confirmAddress(results[0])
|
|
431
|
+
} else {
|
|
432
|
+
this.log('skipping verifying %a because we are too close to the connection limit', results[0]?.multiaddr)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.queue.add(async (options: AbortOptions) => {
|
|
439
|
+
const signal = anySignal([options.signal, AbortSignal.timeout(this.timeout)])
|
|
440
|
+
const nonce = BigInt(randomNumber(0, Number.MAX_SAFE_INTEGER))
|
|
441
|
+
this.nonces.add(nonce)
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await this.askPeerToVerify(connection, segment, nonce, options)
|
|
445
|
+
} finally {
|
|
446
|
+
signal.clear()
|
|
447
|
+
this.nonces.delete(nonce)
|
|
448
|
+
}
|
|
449
|
+
}, {
|
|
450
|
+
peerId: connection.remotePeer
|
|
451
|
+
})
|
|
452
|
+
.catch(err => {
|
|
453
|
+
this.log.error('error from %p verifying addresses - %e', connection.remotePeer, err)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async askPeerToVerify (connection: Connection, segment: string, nonce: bigint, options: AbortOptions): Promise<void> {
|
|
458
|
+
const unverifiedAddresses = [...this.dialResults.values()]
|
|
459
|
+
.filter(entry => entry.result == null)
|
|
460
|
+
.map(entry => entry.multiaddr)
|
|
461
|
+
|
|
462
|
+
if (unverifiedAddresses.length === 0) {
|
|
463
|
+
// no unverified addresses
|
|
464
|
+
this.queue.clear()
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.log.trace('asking %p to verify multiaddrs %s', connection.remotePeer, unverifiedAddresses)
|
|
469
|
+
|
|
470
|
+
const stream = await connection.newStream(this.dialRequestProtocol, options)
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
const messages = pbStream(stream).pb(Message)
|
|
474
|
+
await messages.write({
|
|
475
|
+
dialRequest: {
|
|
476
|
+
addrs: unverifiedAddresses.map(ma => ma.bytes),
|
|
477
|
+
nonce
|
|
478
|
+
}
|
|
479
|
+
}, options)
|
|
480
|
+
|
|
481
|
+
while (true) {
|
|
482
|
+
let response = await messages.read(options)
|
|
483
|
+
|
|
484
|
+
if (response.dialDataRequest != null) {
|
|
485
|
+
if (response.dialDataRequest.numBytes > this.maxDialDataBytes) {
|
|
486
|
+
this.log('too many dial data byte requested by %p - %s/%s', connection.remotePeer, response.dialDataRequest.numBytes, this.maxDialDataBytes)
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.log('sending %d bytes to %p as anti-amplification attack protection', response.dialDataRequest.numBytes, connection.remotePeer)
|
|
491
|
+
|
|
492
|
+
const buf = new Uint8Array(this.dialDataChunkSize)
|
|
493
|
+
const bufSize = BigInt(this.dialDataChunkSize)
|
|
494
|
+
|
|
495
|
+
for (let i = 0n; i < response.dialDataRequest.numBytes; i += bufSize) {
|
|
496
|
+
await messages.write({
|
|
497
|
+
dialDataResponse: {
|
|
498
|
+
data: buf
|
|
499
|
+
}
|
|
500
|
+
}, options)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
response = await messages.read(options)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (response.dialResponse == null) {
|
|
507
|
+
this.log('invalid autonat response from %p - %j', connection.remotePeer, response)
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const status = response.dialResponse.status
|
|
512
|
+
|
|
513
|
+
if (status !== DialResponse.ResponseStatus.OK) {
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const dialed = unverifiedAddresses[response.dialResponse.addrIdx]
|
|
518
|
+
|
|
519
|
+
if (dialed == null) {
|
|
520
|
+
this.log.trace('peer dialed unknown address')
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const results = this.dialResults.get(dialed.toString())
|
|
525
|
+
|
|
526
|
+
if (results == null) {
|
|
527
|
+
this.log.trace('peer reported %a as %s but there is no result object', dialed, response.dialResponse.status)
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (results.networkSegments.includes(segment)) {
|
|
532
|
+
this.log.trace('%a results already included network segment %s', dialed, segment)
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (results.result != null) {
|
|
537
|
+
this.log.trace('already resolved result for %a, ignoring response from', dialed, connection.remotePeer)
|
|
538
|
+
continue
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (results.verifyingPeers.has(connection.remotePeer)) {
|
|
542
|
+
this.log.trace('peer %p has already verified %a, ignoring response', connection.remotePeer, dialed)
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
results.verifyingPeers.add(connection.remotePeer)
|
|
547
|
+
results.networkSegments.push(segment)
|
|
548
|
+
|
|
549
|
+
if (response.dialResponse.dialStatus === DialStatus.OK) {
|
|
550
|
+
this.log.trace('%p dialed %a successfully', connection.remotePeer, results.multiaddr)
|
|
551
|
+
|
|
552
|
+
results.success++
|
|
553
|
+
|
|
554
|
+
// observed addresses require more confirmations
|
|
555
|
+
if (results.type !== 'observed') {
|
|
556
|
+
this.confirmAddress(results)
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
} else if (response.dialResponse.dialStatus === DialStatus.E_DIAL_ERROR) {
|
|
560
|
+
this.log.trace('%p could not dial %a', connection.remotePeer, results.multiaddr)
|
|
561
|
+
// the address was not dialable (e.g. not public)
|
|
562
|
+
results.failure++
|
|
563
|
+
} else if (response.dialResponse.dialStatus === DialStatus.E_DIAL_BACK_ERROR) {
|
|
564
|
+
this.log.trace('%p saw error while dialing %a', connection.remotePeer, results.multiaddr)
|
|
565
|
+
// the address was dialable but an error occurred during the dial back
|
|
566
|
+
continue
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.log('%a success %d failure %d', results.multiaddr, results.success, results.failure)
|
|
570
|
+
|
|
571
|
+
if (results.success === REQUIRED_SUCCESSFUL_DIALS) {
|
|
572
|
+
this.confirmAddress(results)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (results.failure === REQUIRED_FAILED_DIALS) {
|
|
576
|
+
this.unconfirmAddress(results)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
try {
|
|
581
|
+
await stream.close(options)
|
|
582
|
+
} catch (err: any) {
|
|
583
|
+
stream.abort(err)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private hasConnectionCapacity (): boolean {
|
|
589
|
+
const connections = this.components.connectionManager.getConnections()
|
|
590
|
+
const currentConnectionCount = connections.length
|
|
591
|
+
const maxConnections = this.components.connectionManager.getMaxConnections()
|
|
592
|
+
|
|
593
|
+
return ((currentConnectionCount / maxConnections) * 100) < this.connectionThreshold
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private confirmAddress (results: DialResults): void {
|
|
597
|
+
// we are now convinced
|
|
598
|
+
this.log('%s address %a is externally dialable', results.type, results.multiaddr)
|
|
599
|
+
this.components.addressManager.confirmObservedAddr(results.multiaddr)
|
|
600
|
+
this.dialResults.delete(results.multiaddr.toString())
|
|
601
|
+
|
|
602
|
+
// abort & remove any outstanding verification jobs for this multiaddr
|
|
603
|
+
results.result = true
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private unconfirmAddress (results: DialResults): void {
|
|
607
|
+
// we are now unconvinced
|
|
608
|
+
this.log('%s address %a is not externally dialable', results.type, results.multiaddr)
|
|
609
|
+
this.components.addressManager.removeObservedAddr(results.multiaddr)
|
|
610
|
+
this.dialResults.delete(results.multiaddr.toString())
|
|
611
|
+
|
|
612
|
+
// abort & remove any outstanding verification jobs for this multiaddr
|
|
613
|
+
results.result = false
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private getNetworkSegment (ma: Multiaddr): string {
|
|
617
|
+
// make sure we use different network segments
|
|
618
|
+
const options = ma.toOptions()
|
|
619
|
+
|
|
620
|
+
if (options.family === 4) {
|
|
621
|
+
const octets = options.host.split('.')
|
|
622
|
+
return octets[0].padStart(3, '0')
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const octets = options.host.split(':')
|
|
626
|
+
return octets[0].padStart(4, '0')
|
|
627
|
+
}
|
|
628
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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 = '2'
|
|
15
|
+
export const TIMEOUT = 30_000
|
|
16
|
+
export const MAX_INBOUND_STREAMS = 2
|
|
17
|
+
export const MAX_OUTBOUND_STREAMS = 20
|
|
18
|
+
export const DEFAULT_CONNECTION_THRESHOLD = 80
|
|
19
|
+
export const MAX_MESSAGE_SIZE = 8192
|
|
20
|
+
|
|
21
|
+
export const DIAL_REQUEST = 'dial-request'
|
|
22
|
+
export const DIAL_BACK = 'dial-back'
|
|
23
|
+
export const MAX_DIAL_DATA_BYTES = 200n * 1024n
|
|
24
|
+
export const DIAL_DATA_CHUNK_SIZE = 4096
|