@libp2p/dcutr 0.0.0-97ab31c0c

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/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@libp2p/dcutr",
3
+ "version": "0.0.0-97ab31c0c",
4
+ "description": "Implementation of the DCUtR Protocol",
5
+ "license": "Apache-2.0 OR MIT",
6
+ "homepage": "https://github.com/libp2p/js-libp2p/tree/master/packages/protocol-dcutr#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/libp2p/js-libp2p.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/libp2p/js-libp2p/issues"
13
+ },
14
+ "type": "module",
15
+ "types": "./dist/src/index.d.ts",
16
+ "files": [
17
+ "src",
18
+ "dist",
19
+ "!dist/test",
20
+ "!**/*.tsbuildinfo"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/src/index.d.ts",
25
+ "import": "./dist/src/index.js"
26
+ }
27
+ },
28
+ "eslintConfig": {
29
+ "extends": "ipfs",
30
+ "parserOptions": {
31
+ "project": true,
32
+ "sourceType": "module"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "start": "node dist/src/main.js",
37
+ "build": "aegir build",
38
+ "test": "aegir test",
39
+ "clean": "aegir clean",
40
+ "generate": "protons ./src/pb/index.proto",
41
+ "lint": "aegir lint",
42
+ "test:chrome": "aegir test -t browser --cov",
43
+ "test:chrome-webworker": "aegir test -t webworker",
44
+ "test:firefox": "aegir test -t browser -- --browser firefox",
45
+ "test:firefox-webworker": "aegir test -t webworker -- --browser firefox",
46
+ "test:node": "aegir test -t node --cov",
47
+ "dep-check": "aegir dep-check"
48
+ },
49
+ "dependencies": {
50
+ "@libp2p/interface": "0.1.6-97ab31c0c",
51
+ "@libp2p/interface-internal": "0.1.9-97ab31c0c",
52
+ "@multiformats/multiaddr": "^12.1.10",
53
+ "@multiformats/multiaddr-matcher": "^1.1.0",
54
+ "delay": "^6.0.0",
55
+ "it-protobuf-stream": "^1.0.2",
56
+ "private-ip": "^3.0.1",
57
+ "protons-runtime": "^5.0.0",
58
+ "uint8arraylist": "^2.4.3"
59
+ },
60
+ "devDependencies": {
61
+ "aegir": "^41.0.2",
62
+ "protons": "^7.3.0",
63
+ "sinon": "^17.0.0",
64
+ "sinon-ts": "^2.0.0"
65
+ }
66
+ }
package/src/dcutr.ts ADDED
@@ -0,0 +1,377 @@
1
+ import { CodeError, ERR_INVALID_MESSAGE } from '@libp2p/interface/errors'
2
+ import { type Multiaddr, multiaddr } from '@multiformats/multiaddr'
3
+ import delay from 'delay'
4
+ import { pbStream } from 'it-protobuf-stream'
5
+ import { HolePunch } from './pb/message.js'
6
+ import { isPublicAndDialable } from './utils.js'
7
+ import { multicodec } from './index.js'
8
+ import type { DCUtRServiceComponents, DCUtRServiceInit } from './index.js'
9
+ import type { Logger } from '@libp2p/interface'
10
+ import type { Connection, Stream } from '@libp2p/interface/connection'
11
+ import type { PeerStore } from '@libp2p/interface/peer-store'
12
+ import type { Startable } from '@libp2p/interface/startable'
13
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager'
14
+ import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
15
+ import type { Registrar } from '@libp2p/interface-internal/registrar'
16
+ import type { TransportManager } from '@libp2p/interface-internal/src/transport-manager/index.js'
17
+
18
+ // https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#rpc-messages
19
+ const MAX_DCUTR_MESSAGE_SIZE = 1024 * 4
20
+ // ensure the dial has a high priority to jump to the head of the dial queue
21
+ const DCUTR_DIAL_PRIORITY = 100
22
+
23
+ const defaultValues = {
24
+ // https://github.com/libp2p/go-libp2p/blob/8d2e54e1637041d5cf4fac1e531287560bd1f4ac/p2p/protocol/holepunch/holepuncher.go#L27
25
+ timeout: 5000,
26
+ // https://github.com/libp2p/go-libp2p/blob/8d2e54e1637041d5cf4fac1e531287560bd1f4ac/p2p/protocol/holepunch/holepuncher.go#L28
27
+ retries: 3,
28
+ maxInboundStreams: 1,
29
+ maxOutboundStreams: 1
30
+ }
31
+
32
+ export class DefaultDCUtRService implements Startable {
33
+ private started: boolean
34
+ private readonly timeout: number
35
+ private readonly retries: number
36
+ private readonly maxInboundStreams: number
37
+ private readonly maxOutboundStreams: number
38
+ private readonly peerStore: PeerStore
39
+ private readonly registrar: Registrar
40
+ private readonly connectionManager: ConnectionManager
41
+ private readonly addressManager: AddressManager
42
+ private readonly transportManager: TransportManager
43
+ private topologyId?: string
44
+ private readonly log: Logger
45
+
46
+ constructor (components: DCUtRServiceComponents, init: DCUtRServiceInit) {
47
+ this.log = components.logger.forComponent('libp2p:dcutr')
48
+ this.started = false
49
+ this.peerStore = components.peerStore
50
+ this.registrar = components.registrar
51
+ this.addressManager = components.addressManager
52
+ this.connectionManager = components.connectionManager
53
+ this.transportManager = components.transportManager
54
+
55
+ this.timeout = init.timeout ?? defaultValues.timeout
56
+ this.retries = init.retries ?? defaultValues.retries
57
+ this.maxInboundStreams = init.maxInboundStreams ?? defaultValues.maxInboundStreams
58
+ this.maxOutboundStreams = init.maxOutboundStreams ?? defaultValues.maxOutboundStreams
59
+ }
60
+
61
+ isStarted (): boolean {
62
+ return this.started
63
+ }
64
+
65
+ async start (): Promise<void> {
66
+ if (this.started) {
67
+ return
68
+ }
69
+
70
+ // register for notifications of when peers that support DCUtR connect
71
+ // nb. requires the identify service to be enabled
72
+ this.topologyId = await this.registrar.register(multicodec, {
73
+ notifyOnTransient: true,
74
+ onConnect: (peerId, connection) => {
75
+ if (!connection.transient) {
76
+ // the connection is already direct, no upgrade is required
77
+ return
78
+ }
79
+
80
+ // the inbound peer starts the connection upgrade
81
+ if (connection.direction !== 'inbound') {
82
+ return
83
+ }
84
+
85
+ this.upgradeInbound(connection)
86
+ .catch(err => {
87
+ this.log.error('error during outgoing DCUtR attempt', err)
88
+ })
89
+ }
90
+ })
91
+
92
+ await this.registrar.handle(multicodec, (data) => {
93
+ void this.handleIncomingUpgrade(data.stream, data.connection).catch(err => {
94
+ this.log.error('error during incoming DCUtR attempt', err)
95
+ data.stream.abort(err)
96
+ })
97
+ }, {
98
+ maxInboundStreams: this.maxInboundStreams,
99
+ maxOutboundStreams: this.maxOutboundStreams,
100
+ runOnTransientConnection: true
101
+ })
102
+
103
+ this.started = true
104
+ }
105
+
106
+ async stop (): Promise<void> {
107
+ await this.registrar.unhandle(multicodec)
108
+
109
+ if (this.topologyId != null) {
110
+ this.registrar.unregister(this.topologyId)
111
+ }
112
+
113
+ this.started = false
114
+ }
115
+
116
+ /**
117
+ * Perform the inbound connection upgrade as B
118
+ *
119
+ * @see https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#the-protocol
120
+ */
121
+ async upgradeInbound (relayedConnection: Connection): Promise<void> {
122
+ // Upon observing the new connection, the inbound peer (here B) checks the
123
+ // addresses advertised by A via identify.
124
+ //
125
+ // If that set includes public addresses, then A may be reachable by a direct
126
+ // connection, in which case B attempts a unilateral connection upgrade by
127
+ // initiating a direct connection to A.
128
+ if (await this.attemptUnilateralConnectionUpgrade(relayedConnection)) {
129
+ return
130
+ }
131
+
132
+ let stream: Stream | undefined
133
+
134
+ for (let i = 0; i < this.retries; i++) {
135
+ const options = {
136
+ signal: AbortSignal.timeout(this.timeout)
137
+ }
138
+
139
+ try {
140
+ // 1. B opens a stream to A using the /libp2p/dcutr protocol.
141
+ stream = await relayedConnection.newStream([multicodec], {
142
+ signal: options.signal,
143
+ runOnTransientConnection: true
144
+ })
145
+
146
+ const pb = pbStream(stream, {
147
+ maxDataLength: MAX_DCUTR_MESSAGE_SIZE
148
+ }).pb(HolePunch)
149
+
150
+ // 2. B sends to A a Connect message containing its observed (and
151
+ // possibly predicted) addresses from identify and starts a timer
152
+ // to measure RTT of the relay connection.
153
+ this.log('B sending connect to %p', relayedConnection.remotePeer)
154
+ const connectTimer = Date.now()
155
+ await pb.write({
156
+ type: HolePunch.Type.CONNECT,
157
+ observedAddresses: this.addressManager.getAddresses().map(ma => ma.bytes)
158
+ }, options)
159
+
160
+ this.log('B receiving connect from %p', relayedConnection.remotePeer)
161
+ // 4. Upon receiving the Connect, B sends a Sync message
162
+ const connect = await pb.read(options)
163
+
164
+ if (connect.type !== HolePunch.Type.CONNECT) {
165
+ this.log('A sent wrong message type')
166
+ throw new CodeError('DCUtR message type was incorrect', ERR_INVALID_MESSAGE)
167
+ }
168
+
169
+ const multiaddrs = this.getDialableMultiaddrs(connect.observedAddresses)
170
+
171
+ if (multiaddrs.length === 0) {
172
+ this.log('A did not have any dialable multiaddrs')
173
+ throw new CodeError('DCUtR connect message had no multiaddrs', ERR_INVALID_MESSAGE)
174
+ }
175
+
176
+ const rtt = Date.now() - connectTimer
177
+
178
+ this.log('A sending sync, rtt %dms', rtt)
179
+ await pb.write({
180
+ type: HolePunch.Type.SYNC,
181
+ observedAddresses: []
182
+ }, options)
183
+
184
+ this.log('A waiting for half RTT')
185
+ // ..and starts a timer for half the RTT measured from the time between
186
+ // sending the initial Connect and receiving the response
187
+ await delay(rtt / 2)
188
+
189
+ // TODO: when we have a QUIC transport, the dial step is different - for
190
+ // now we only have tcp support
191
+ // https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#the-protocol
192
+
193
+ this.log('B dialing', multiaddrs)
194
+ // Upon expiry of the timer, B dials the address to A.
195
+ const conn = await this.connectionManager.openConnection(multiaddrs, {
196
+ signal: options.signal,
197
+ priority: DCUTR_DIAL_PRIORITY
198
+ })
199
+
200
+ this.log('DCUtR to %p succeeded to address %a, closing relayed connection', relayedConnection.remotePeer, conn.remoteAddr)
201
+ await relayedConnection.close(options)
202
+
203
+ break
204
+ } catch (err: any) {
205
+ this.log.error('error while attempting DCUtR on attempt %d of %d', i + 1, this.retries, err)
206
+ stream?.abort(err)
207
+
208
+ if (i === this.retries) {
209
+ throw err
210
+ }
211
+ } finally {
212
+ if (stream != null) {
213
+ await stream.close(options)
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * This is performed when A has dialed B via a relay but A also has a public
221
+ * address that B can dial directly
222
+ */
223
+ async attemptUnilateralConnectionUpgrade (relayedConnection: Connection): Promise<boolean> {
224
+ // Upon observing the new connection, the inbound peer (here B) checks the
225
+ // addresses advertised by A via identify.
226
+ const peerInfo = await this.peerStore.get(relayedConnection.remotePeer)
227
+
228
+ // If that set includes public addresses, then A may be reachable by a direct
229
+ // connection, in which case B attempts a unilateral connection upgrade by
230
+ // initiating a direct connection to A.
231
+ const publicAddresses = peerInfo.addresses
232
+ .map(address => {
233
+ const ma = address.multiaddr
234
+
235
+ // ensure all multiaddrs have the peer id
236
+ if (ma.getPeerId() == null) {
237
+ return ma.encapsulate(`/p2p/${relayedConnection.remotePeer}`)
238
+ }
239
+
240
+ return ma
241
+ })
242
+ .filter(ma => {
243
+ return isPublicAndDialable(ma, this.transportManager)
244
+ })
245
+
246
+ if (publicAddresses.length > 0) {
247
+ const signal = AbortSignal.timeout(this.timeout)
248
+
249
+ try {
250
+ this.log('attempting unilateral connection upgrade to %a', publicAddresses)
251
+
252
+ // force-dial the multiaddr(s), otherwise `connectionManager.openConnection`
253
+ // will return the existing relayed connection
254
+ const connection = await this.connectionManager.openConnection(publicAddresses, {
255
+ signal,
256
+ force: true
257
+ })
258
+
259
+ if (connection.transient) {
260
+ throw new Error('Could not open a new, non-transient, connection')
261
+ }
262
+
263
+ this.log('unilateral connection upgrade to %p succeeded via %a, closing relayed connection', relayedConnection.remotePeer, connection.remoteAddr)
264
+
265
+ await relayedConnection.close({
266
+ signal
267
+ })
268
+
269
+ return true
270
+ } catch (err) {
271
+ this.log.error('unilateral connection upgrade to %p on addresses %a failed', relayedConnection.remotePeer, publicAddresses, err)
272
+ }
273
+ } else {
274
+ this.log('peer %p has no public addresses, not attempting unilateral connection upgrade', relayedConnection.remotePeer)
275
+ }
276
+
277
+ // no public addresses or failed to dial public addresses
278
+ return false
279
+ }
280
+
281
+ /**
282
+ * Perform the connection upgrade as A
283
+ *
284
+ * @see https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#the-protocol
285
+ */
286
+ async handleIncomingUpgrade (stream: Stream, relayedConnection: Connection): Promise<void> {
287
+ const options = {
288
+ signal: AbortSignal.timeout(this.timeout)
289
+ }
290
+
291
+ try {
292
+ const pb = pbStream(stream, {
293
+ maxDataLength: MAX_DCUTR_MESSAGE_SIZE
294
+ }).pb(HolePunch)
295
+
296
+ this.log('A receiving connect')
297
+ // 3. Upon receiving the Connect, A responds back with a Connect message
298
+ // containing its observed (and possibly predicted) addresses.
299
+ const connect = await pb.read(options)
300
+
301
+ if (connect.type !== HolePunch.Type.CONNECT) {
302
+ this.log('B sent wrong message type')
303
+ throw new CodeError('DCUtR message type was incorrect', ERR_INVALID_MESSAGE)
304
+ }
305
+
306
+ if (connect.observedAddresses.length === 0) {
307
+ this.log('B sent no multiaddrs')
308
+ throw new CodeError('DCUtR connect message had no multiaddrs', ERR_INVALID_MESSAGE)
309
+ }
310
+
311
+ const multiaddrs = this.getDialableMultiaddrs(connect.observedAddresses)
312
+
313
+ if (multiaddrs.length === 0) {
314
+ this.log('B had no dialable multiaddrs')
315
+ throw new CodeError('DCUtR connect message had no dialable multiaddrs', ERR_INVALID_MESSAGE)
316
+ }
317
+
318
+ this.log('A sending connect')
319
+ await pb.write({
320
+ type: HolePunch.Type.CONNECT,
321
+ observedAddresses: this.addressManager.getAddresses().map(ma => ma.bytes)
322
+ })
323
+
324
+ this.log('A receiving sync')
325
+ const sync = await pb.read(options)
326
+
327
+ if (sync.type !== HolePunch.Type.SYNC) {
328
+ throw new CodeError('DCUtR message type was incorrect', ERR_INVALID_MESSAGE)
329
+ }
330
+
331
+ // TODO: when we have a QUIC transport, the dial step is different - for
332
+ // now we only have tcp support
333
+ // https://github.com/libp2p/specs/blob/master/relay/DCUtR.md#the-protocol
334
+
335
+ // Upon receiving the Sync, A immediately dials the address to B
336
+ this.log('A dialing', multiaddrs)
337
+ const connection = await this.connectionManager.openConnection(multiaddrs, {
338
+ signal: options.signal,
339
+ priority: DCUTR_DIAL_PRIORITY,
340
+ force: true
341
+ })
342
+
343
+ this.log('DCUtR to %p succeeded via %a, closing relayed connection', relayedConnection.remotePeer, connection.remoteAddr)
344
+ await relayedConnection.close(options)
345
+ } catch (err: any) {
346
+ this.log.error('incoming DCUtR from %p failed', relayedConnection.remotePeer, err)
347
+ stream.abort(err)
348
+ } finally {
349
+ await stream.close(options)
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Takes the `addr` and converts it to a Multiaddr if possible
355
+ */
356
+ getDialableMultiaddrs (addrs: Array<Uint8Array | string | null | undefined>): Multiaddr[] {
357
+ const output = []
358
+
359
+ for (const addr of addrs) {
360
+ if (addr == null || addr.length === 0) {
361
+ continue
362
+ }
363
+
364
+ try {
365
+ const ma = multiaddr(addr)
366
+
367
+ if (!isPublicAndDialable(ma, this.transportManager)) {
368
+ continue
369
+ }
370
+
371
+ output.push(ma)
372
+ } catch {}
373
+ }
374
+
375
+ return output
376
+ }
377
+ }
package/src/index.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * Direct Connection Upgrade through Relay (DCUtR) is a protocol that allows two
5
+ * nodes to connect to each other who would otherwise be prevented doing so due
6
+ * to being behind NATed connections or firewalls.
7
+ *
8
+ * The protocol involves making a relayed connection between the two peers and
9
+ * using the relay to synchronise connection timings so that they dial each other
10
+ * at precisely the same moment.
11
+ *
12
+ * @example
13
+ *
14
+ * ```ts
15
+ * import { createLibp2p } from 'libp2p'
16
+ * import { circuitRelayTransport } from 'libp2p/circuit-relay'
17
+ * import { tcp } from '@libp2p/tcp'
18
+ * import { identify } from '@libp2p/identify'
19
+ * import { dcutr } from '@libp2p/dcutr'
20
+ *
21
+ * const node = await createLibp2p({
22
+ * transports: [
23
+ * circuitRelayTransport(),
24
+ * tcp()
25
+ * ],
26
+ * services: {
27
+ * identify: identify(),
28
+ * dcutr: dcutr()
29
+ * }
30
+ * })
31
+ *
32
+ * // QmTarget is a peer that is behind a NAT, supports TCP and has a relay
33
+ * // reservation
34
+ * await node.dial('/ip4/.../p2p/QmRelay/p2p-circuit/p2p/QmTarget')
35
+ *
36
+ * // after a while the connection should automatically get upgraded to a
37
+ * // direct connection (e.g. non-transient)
38
+ * while (true) {
39
+ * const connections = node.getConnections()
40
+ *
41
+ * if (connections.find(conn => conn.transient === false)) {
42
+ * console.info('have direct connection')
43
+ * break
44
+ * } else {
45
+ * console.info('have relayed connection')
46
+ *
47
+ * // wait a few seconds to see if it's succeeded yet
48
+ * await new Promise((resolve) => {
49
+ * setTimeout(() => resolve(), 5000)
50
+ * })
51
+ * }
52
+ * }
53
+ * ```
54
+ */
55
+
56
+ import { DefaultDCUtRService } from './dcutr.js'
57
+ import type { ComponentLogger } from '@libp2p/interface'
58
+ import type { PeerStore } from '@libp2p/interface/peer-store'
59
+ import type { AddressManager } from '@libp2p/interface-internal/address-manager'
60
+ import type { ConnectionManager } from '@libp2p/interface-internal/connection-manager'
61
+ import type { Registrar } from '@libp2p/interface-internal/registrar'
62
+ import type { TransportManager } from '@libp2p/interface-internal/transport-manager'
63
+
64
+ export interface DCUtRServiceInit {
65
+ /**
66
+ * How long we should wait for the connection upgrade to complete (default: 5s)
67
+ */
68
+ timeout?: number
69
+
70
+ /**
71
+ * How many times to retry the connection upgrade (default: 3)
72
+ */
73
+ retries?: number
74
+
75
+ /**
76
+ * How many simultaneous inbound DCUtR protocol streams to allow on each
77
+ * connection (default: 1)
78
+ */
79
+ maxInboundStreams?: number
80
+
81
+ /**
82
+ * How many simultaneous outbound DCUtR protocol streams to allow on each
83
+ * connection (default: 1)
84
+ */
85
+ maxOutboundStreams?: number
86
+ }
87
+
88
+ export interface DCUtRServiceComponents {
89
+ peerStore: PeerStore
90
+ connectionManager: ConnectionManager
91
+ registrar: Registrar
92
+ addressManager: AddressManager
93
+ transportManager: TransportManager
94
+ logger: ComponentLogger
95
+ }
96
+
97
+ /**
98
+ * The DCUtR protocol
99
+ */
100
+ export const multicodec = '/libp2p/dcutr'
101
+
102
+ export function dcutr (init: DCUtRServiceInit = {}): (components: DCUtRServiceComponents) => unknown {
103
+ return (components) => new DefaultDCUtRService(components, init)
104
+ }
@@ -0,0 +1,12 @@
1
+ syntax = "proto3";
2
+
3
+ message HolePunch {
4
+ enum Type {
5
+ UNUSED = 0;
6
+ CONNECT = 100;
7
+ SYNC = 300;
8
+ }
9
+
10
+ optional Type type = 1;
11
+ repeated bytes observed_addresses = 2;
12
+ }
@@ -0,0 +1,96 @@
1
+ /* eslint-disable import/export */
2
+ /* eslint-disable complexity */
3
+ /* eslint-disable @typescript-eslint/no-namespace */
4
+ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
5
+ /* eslint-disable @typescript-eslint/no-empty-interface */
6
+
7
+ import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime'
8
+ import type { Codec } from 'protons-runtime'
9
+ import type { Uint8ArrayList } from 'uint8arraylist'
10
+
11
+ export interface HolePunch {
12
+ type?: HolePunch.Type
13
+ observedAddresses: Uint8Array[]
14
+ }
15
+
16
+ export namespace HolePunch {
17
+ export enum Type {
18
+ UNUSED = 'UNUSED',
19
+ CONNECT = 'CONNECT',
20
+ SYNC = 'SYNC'
21
+ }
22
+
23
+ enum __TypeValues {
24
+ UNUSED = 0,
25
+ CONNECT = 100,
26
+ SYNC = 300
27
+ }
28
+
29
+ export namespace Type {
30
+ export const codec = (): Codec<Type> => {
31
+ return enumeration<Type>(__TypeValues)
32
+ }
33
+ }
34
+
35
+ let _codec: Codec<HolePunch>
36
+
37
+ export const codec = (): Codec<HolePunch> => {
38
+ if (_codec == null) {
39
+ _codec = message<HolePunch>((obj, w, opts = {}) => {
40
+ if (opts.lengthDelimited !== false) {
41
+ w.fork()
42
+ }
43
+
44
+ if (obj.type != null) {
45
+ w.uint32(8)
46
+ HolePunch.Type.codec().encode(obj.type, w)
47
+ }
48
+
49
+ if (obj.observedAddresses != null) {
50
+ for (const value of obj.observedAddresses) {
51
+ w.uint32(18)
52
+ w.bytes(value)
53
+ }
54
+ }
55
+
56
+ if (opts.lengthDelimited !== false) {
57
+ w.ldelim()
58
+ }
59
+ }, (reader, length) => {
60
+ const obj: any = {
61
+ observedAddresses: []
62
+ }
63
+
64
+ const end = length == null ? reader.len : reader.pos + length
65
+
66
+ while (reader.pos < end) {
67
+ const tag = reader.uint32()
68
+
69
+ switch (tag >>> 3) {
70
+ case 1:
71
+ obj.type = HolePunch.Type.codec().decode(reader)
72
+ break
73
+ case 2:
74
+ obj.observedAddresses.push(reader.bytes())
75
+ break
76
+ default:
77
+ reader.skipType(tag & 7)
78
+ break
79
+ }
80
+ }
81
+
82
+ return obj
83
+ })
84
+ }
85
+
86
+ return _codec
87
+ }
88
+
89
+ export const encode = (obj: Partial<HolePunch>): Uint8Array => {
90
+ return encodeMessage(obj, HolePunch.codec())
91
+ }
92
+
93
+ export const decode = (buf: Uint8Array | Uint8ArrayList): HolePunch => {
94
+ return decodeMessage(buf, HolePunch.codec())
95
+ }
96
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type Multiaddr } from '@multiformats/multiaddr'
2
+ import { Circuit, IP, DNS } from '@multiformats/multiaddr-matcher'
3
+ import isPrivate from 'private-ip'
4
+ import type { TransportManager } from '@libp2p/interface-internal/src/transport-manager'
5
+
6
+ /**
7
+ * Returns true if the passed multiaddr is public, not relayed and we have a
8
+ * transport that can dial it
9
+ */
10
+ export function isPublicAndDialable (ma: Multiaddr, transportManager: TransportManager): boolean {
11
+ // ignore circuit relay
12
+ if (Circuit.matches(ma)) {
13
+ return false
14
+ }
15
+
16
+ const transport = transportManager.transportForMultiaddr(ma)
17
+
18
+ if (transport == null) {
19
+ return false
20
+ }
21
+
22
+ // dns addresses are probably public?
23
+ if (DNS.matches(ma)) {
24
+ return true
25
+ }
26
+
27
+ // ensure we have only IPv4/IPv6 addresses
28
+ if (!IP.matches(ma)) {
29
+ return false
30
+ }
31
+
32
+ return isPrivate(ma.toOptions().host) === false
33
+ }