@libp2p/circuit-relay-v2 0.0.0 → 1.0.0-06e6d235f

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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/dist/index.min.js +7 -7
  3. package/dist/src/index.d.ts +1 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/pb/index.js.map +1 -1
  6. package/dist/src/server/advert-service.d.ts +2 -4
  7. package/dist/src/server/advert-service.d.ts.map +1 -1
  8. package/dist/src/server/advert-service.js +1 -1
  9. package/dist/src/server/advert-service.js.map +1 -1
  10. package/dist/src/server/index.d.ts +2 -8
  11. package/dist/src/server/index.d.ts.map +1 -1
  12. package/dist/src/server/index.js +1 -1
  13. package/dist/src/server/index.js.map +1 -1
  14. package/dist/src/server/reservation-store.d.ts +1 -3
  15. package/dist/src/server/reservation-store.d.ts.map +1 -1
  16. package/dist/src/server/reservation-store.js.map +1 -1
  17. package/dist/src/server/reservation-voucher.d.ts +1 -2
  18. package/dist/src/server/reservation-voucher.d.ts.map +1 -1
  19. package/dist/src/server/reservation-voucher.js.map +1 -1
  20. package/dist/src/transport/discovery.d.ts +3 -9
  21. package/dist/src/transport/discovery.d.ts.map +1 -1
  22. package/dist/src/transport/discovery.js +1 -1
  23. package/dist/src/transport/discovery.js.map +1 -1
  24. package/dist/src/transport/index.d.ts +2 -10
  25. package/dist/src/transport/index.d.ts.map +1 -1
  26. package/dist/src/transport/index.js +4 -273
  27. package/dist/src/transport/index.js.map +1 -1
  28. package/dist/src/transport/listener.d.ts +2 -3
  29. package/dist/src/transport/listener.d.ts.map +1 -1
  30. package/dist/src/transport/listener.js +2 -2
  31. package/dist/src/transport/listener.js.map +1 -1
  32. package/dist/src/transport/reservation-store.d.ts +2 -7
  33. package/dist/src/transport/reservation-store.d.ts.map +1 -1
  34. package/dist/src/transport/reservation-store.js +17 -1
  35. package/dist/src/transport/reservation-store.js.map +1 -1
  36. package/dist/src/transport/transport.d.ts +60 -0
  37. package/dist/src/transport/transport.d.ts.map +1 -0
  38. package/dist/src/transport/transport.js +286 -0
  39. package/dist/src/transport/transport.js.map +1 -0
  40. package/dist/src/utils.d.ts +1 -2
  41. package/dist/src/utils.d.ts.map +1 -1
  42. package/dist/src/utils.js +4 -10
  43. package/dist/src/utils.js.map +1 -1
  44. package/package.json +16 -11
  45. package/src/index.ts +1 -1
  46. package/src/server/advert-service.ts +2 -4
  47. package/src/server/index.ts +3 -10
  48. package/src/server/reservation-store.ts +1 -3
  49. package/src/server/reservation-voucher.ts +1 -2
  50. package/src/transport/discovery.ts +3 -9
  51. package/src/transport/index.ts +5 -343
  52. package/src/transport/listener.ts +4 -6
  53. package/src/transport/reservation-store.ts +21 -8
  54. package/src/transport/transport.ts +346 -0
  55. package/src/utils.ts +6 -13
@@ -0,0 +1,346 @@
1
+ import { CodeError } from '@libp2p/interface'
2
+ import { transportSymbol, type Transport, type CreateListenerOptions, type Listener, type Upgrader, type AbortOptions, type ComponentLogger, type Logger, type Connection, type Stream, type ConnectionGater, type PeerId, type PeerStore } from '@libp2p/interface'
3
+ import { peerIdFromBytes, peerIdFromString } from '@libp2p/peer-id'
4
+ import { streamToMaConnection } from '@libp2p/utils/stream-to-ma-conn'
5
+ import * as mafmt from '@multiformats/mafmt'
6
+ import { multiaddr } from '@multiformats/multiaddr'
7
+ import { pbStream } from 'it-protobuf-stream'
8
+ import { CIRCUIT_PROTO_CODE, ERR_HOP_REQUEST_FAILED, ERR_RELAYED_DIAL, MAX_CONNECTIONS, RELAY_V2_HOP_CODEC, RELAY_V2_STOP_CODEC } from '../constants.js'
9
+ import { StopMessage, HopMessage, Status } from '../pb/index.js'
10
+ import { RelayDiscovery } from './discovery.js'
11
+ import { createListener } from './listener.js'
12
+ import { ReservationStore } from './reservation-store.js'
13
+ import type { CircuitRelayTransportComponents, CircuitRelayTransportInit } from './index.js'
14
+ import type { AddressManager, ConnectionManager, IncomingStreamData, Registrar, TransportManager } from '@libp2p/interface-internal'
15
+ import type { Multiaddr } from '@multiformats/multiaddr'
16
+
17
+ const isValidStop = (request: StopMessage): request is Required<StopMessage> => {
18
+ if (request.peer == null) {
19
+ return false
20
+ }
21
+
22
+ try {
23
+ request.peer.addrs.forEach(multiaddr)
24
+ } catch {
25
+ return false
26
+ }
27
+
28
+ return true
29
+ }
30
+
31
+ interface ConnectOptions {
32
+ stream: Stream
33
+ connection: Connection
34
+ destinationPeer: PeerId
35
+ destinationAddr: Multiaddr
36
+ relayAddr: Multiaddr
37
+ ma: Multiaddr
38
+ disconnectOnFailure: boolean
39
+ }
40
+
41
+ const defaults = {
42
+ maxInboundStopStreams: MAX_CONNECTIONS,
43
+ maxOutboundStopStreams: MAX_CONNECTIONS,
44
+ stopTimeout: 30000
45
+ }
46
+
47
+ export class CircuitRelayTransport implements Transport {
48
+ private readonly discovery?: RelayDiscovery
49
+ private readonly registrar: Registrar
50
+ private readonly peerStore: PeerStore
51
+ private readonly connectionManager: ConnectionManager
52
+ private readonly transportManager: TransportManager
53
+ private readonly peerId: PeerId
54
+ private readonly upgrader: Upgrader
55
+ private readonly addressManager: AddressManager
56
+ private readonly connectionGater: ConnectionGater
57
+ public readonly reservationStore: ReservationStore
58
+ private readonly logger: ComponentLogger
59
+ private readonly maxInboundStopStreams: number
60
+ private readonly maxOutboundStopStreams?: number
61
+ private readonly stopTimeout: number
62
+ private started: boolean
63
+ private readonly log: Logger
64
+
65
+ constructor (components: CircuitRelayTransportComponents, init: CircuitRelayTransportInit) {
66
+ this.log = components.logger.forComponent('libp2p:circuit-relay:transport')
67
+ this.registrar = components.registrar
68
+ this.peerStore = components.peerStore
69
+ this.connectionManager = components.connectionManager
70
+ this.transportManager = components.transportManager
71
+ this.logger = components.logger
72
+ this.peerId = components.peerId
73
+ this.upgrader = components.upgrader
74
+ this.addressManager = components.addressManager
75
+ this.connectionGater = components.connectionGater
76
+ this.maxInboundStopStreams = init.maxInboundStopStreams ?? defaults.maxInboundStopStreams
77
+ this.maxOutboundStopStreams = init.maxOutboundStopStreams ?? defaults.maxOutboundStopStreams
78
+ this.stopTimeout = init.stopTimeout ?? defaults.stopTimeout
79
+
80
+ if (init.discoverRelays != null && init.discoverRelays > 0) {
81
+ this.discovery = new RelayDiscovery(components)
82
+ this.discovery.addEventListener('relay:discover', (evt) => {
83
+ this.reservationStore.addRelay(evt.detail, 'discovered')
84
+ .catch(err => {
85
+ this.log.error('could not add discovered relay %p', evt.detail, err)
86
+ })
87
+ })
88
+ }
89
+
90
+ this.reservationStore = new ReservationStore(components, init)
91
+ this.reservationStore.addEventListener('relay:not-enough-relays', () => {
92
+ this.discovery?.discover()
93
+ .catch(err => {
94
+ this.log.error('could not discover relays', err)
95
+ })
96
+ })
97
+
98
+ this.started = false
99
+ }
100
+
101
+ isStarted (): boolean {
102
+ return this.started
103
+ }
104
+
105
+ async start (): Promise<void> {
106
+ await this.reservationStore.start()
107
+ await this.discovery?.start()
108
+
109
+ await this.registrar.handle(RELAY_V2_STOP_CODEC, (data) => {
110
+ void this.onStop(data).catch(err => {
111
+ this.log.error('error while handling STOP protocol', err)
112
+ data.stream.abort(err)
113
+ })
114
+ }, {
115
+ maxInboundStreams: this.maxInboundStopStreams,
116
+ maxOutboundStreams: this.maxOutboundStopStreams,
117
+ runOnTransientConnection: true
118
+ })
119
+
120
+ this.started = true
121
+ }
122
+
123
+ async stop (): Promise<void> {
124
+ this.discovery?.stop()
125
+ await this.reservationStore.stop()
126
+ await this.registrar.unhandle(RELAY_V2_STOP_CODEC)
127
+
128
+ this.started = false
129
+ }
130
+
131
+ readonly [transportSymbol] = true
132
+
133
+ readonly [Symbol.toStringTag] = 'libp2p/circuit-relay-v2'
134
+
135
+ /**
136
+ * Dial a peer over a relay
137
+ */
138
+ async dial (ma: Multiaddr, options: AbortOptions = {}): Promise<Connection> {
139
+ if (ma.protoCodes().filter(code => code === CIRCUIT_PROTO_CODE).length !== 1) {
140
+ const errMsg = 'Invalid circuit relay address'
141
+ this.log.error(errMsg, ma)
142
+ throw new CodeError(errMsg, ERR_RELAYED_DIAL)
143
+ }
144
+
145
+ // Check the multiaddr to see if it contains a relay and a destination peer
146
+ const addrs = ma.toString().split('/p2p-circuit')
147
+ const relayAddr = multiaddr(addrs[0])
148
+ const destinationAddr = multiaddr(addrs[addrs.length - 1])
149
+ const relayId = relayAddr.getPeerId()
150
+ const destinationId = destinationAddr.getPeerId()
151
+
152
+ if (relayId == null || destinationId == null) {
153
+ const errMsg = `Circuit relay dial to ${ma.toString()} failed as address did not have peer ids`
154
+ this.log.error(errMsg)
155
+ throw new CodeError(errMsg, ERR_RELAYED_DIAL)
156
+ }
157
+
158
+ const relayPeer = peerIdFromString(relayId)
159
+ const destinationPeer = peerIdFromString(destinationId)
160
+
161
+ let disconnectOnFailure = false
162
+ const relayConnections = this.connectionManager.getConnections(relayPeer)
163
+ let relayConnection = relayConnections[0]
164
+
165
+ if (relayConnection == null) {
166
+ await this.peerStore.merge(relayPeer, {
167
+ multiaddrs: [relayAddr]
168
+ })
169
+ relayConnection = await this.connectionManager.openConnection(relayPeer, options)
170
+ disconnectOnFailure = true
171
+ }
172
+
173
+ let stream: Stream | undefined
174
+
175
+ try {
176
+ stream = await relayConnection.newStream(RELAY_V2_HOP_CODEC)
177
+
178
+ return await this.connectV2({
179
+ stream,
180
+ connection: relayConnection,
181
+ destinationPeer,
182
+ destinationAddr,
183
+ relayAddr,
184
+ ma,
185
+ disconnectOnFailure
186
+ })
187
+ } catch (err: any) {
188
+ this.log.error('circuit relay dial to destination %p via relay %p failed', destinationPeer, relayPeer, err)
189
+
190
+ if (stream != null) {
191
+ stream.abort(err)
192
+ }
193
+ disconnectOnFailure && await relayConnection.close()
194
+ throw err
195
+ }
196
+ }
197
+
198
+ async connectV2 (
199
+ {
200
+ stream, connection, destinationPeer,
201
+ destinationAddr, relayAddr, ma,
202
+ disconnectOnFailure
203
+ }: ConnectOptions
204
+ ): Promise<Connection> {
205
+ try {
206
+ const pbstr = pbStream(stream)
207
+ const hopstr = pbstr.pb(HopMessage)
208
+ await hopstr.write({
209
+ type: HopMessage.Type.CONNECT,
210
+ peer: {
211
+ id: destinationPeer.toBytes(),
212
+ addrs: [multiaddr(destinationAddr).bytes]
213
+ }
214
+ })
215
+
216
+ const status = await hopstr.read()
217
+
218
+ if (status.status !== Status.OK) {
219
+ throw new CodeError(`failed to connect via relay with status ${status?.status?.toString() ?? 'undefined'}`, ERR_HOP_REQUEST_FAILED)
220
+ }
221
+
222
+ const maConn = streamToMaConnection({
223
+ stream: pbstr.unwrap(),
224
+ remoteAddr: ma,
225
+ localAddr: relayAddr.encapsulate(`/p2p-circuit/p2p/${this.peerId.toString()}`),
226
+ logger: this.logger
227
+ })
228
+
229
+ this.log('new outbound transient connection %a', maConn.remoteAddr)
230
+ return await this.upgrader.upgradeOutbound(maConn, {
231
+ transient: true
232
+ })
233
+ } catch (err: any) {
234
+ this.log.error(`Circuit relay dial to destination ${destinationPeer.toString()} via relay ${connection.remotePeer.toString()} failed`, err)
235
+ disconnectOnFailure && await connection.close()
236
+ throw err
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Create a listener
242
+ */
243
+ createListener (options: CreateListenerOptions): Listener {
244
+ return createListener({
245
+ connectionManager: this.connectionManager,
246
+ relayStore: this.reservationStore,
247
+ logger: this.logger
248
+ })
249
+ }
250
+
251
+ /**
252
+ * Filter check for all Multiaddrs that this transport can dial on
253
+ *
254
+ * @param {Multiaddr[]} multiaddrs
255
+ * @returns {Multiaddr[]}
256
+ */
257
+ filter (multiaddrs: Multiaddr[]): Multiaddr[] {
258
+ multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs]
259
+
260
+ return multiaddrs.filter((ma) => {
261
+ return mafmt.Circuit.matches(ma)
262
+ })
263
+ }
264
+
265
+ /**
266
+ * An incoming STOP request means a remote peer wants to dial us via a relay
267
+ */
268
+ async onStop ({ connection, stream }: IncomingStreamData): Promise<void> {
269
+ if (!this.reservationStore.hasReservation(connection.remotePeer)) {
270
+ try {
271
+ this.log('dialed via relay we did not have a reservation on, start listening on that relay address')
272
+ await this.transportManager.listen([connection.remoteAddr.encapsulate('/p2p-circuit')])
273
+ } catch (err: any) {
274
+ // failed to refresh our hitherto unknown relay reservation but allow the connection attempt anyway
275
+ this.log.error('failed to listen on a relay peer we were dialed via but did not have a reservation on', err)
276
+ }
277
+ }
278
+
279
+ const signal = AbortSignal.timeout(this.stopTimeout)
280
+ const pbstr = pbStream(stream).pb(StopMessage)
281
+ const request = await pbstr.read({
282
+ signal
283
+ })
284
+
285
+ this.log('new circuit relay v2 stop stream from %p with type %s', connection.remotePeer, request.type)
286
+
287
+ if (request?.type === undefined) {
288
+ this.log.error('type was missing from circuit v2 stop protocol request from %s', connection.remotePeer)
289
+ await pbstr.write({ type: StopMessage.Type.STATUS, status: Status.MALFORMED_MESSAGE }, {
290
+ signal
291
+ })
292
+ await stream.close()
293
+ return
294
+ }
295
+
296
+ // Validate the STOP request has the required input
297
+ if (request.type !== StopMessage.Type.CONNECT) {
298
+ this.log.error('invalid stop connect request via peer %p', connection.remotePeer)
299
+ await pbstr.write({ type: StopMessage.Type.STATUS, status: Status.UNEXPECTED_MESSAGE }, {
300
+ signal
301
+ })
302
+ await stream.close()
303
+ return
304
+ }
305
+
306
+ if (!isValidStop(request)) {
307
+ this.log.error('invalid stop connect request via peer %p', connection.remotePeer)
308
+ await pbstr.write({ type: StopMessage.Type.STATUS, status: Status.MALFORMED_MESSAGE }, {
309
+ signal
310
+ })
311
+ await stream.close()
312
+ return
313
+ }
314
+
315
+ const remotePeerId = peerIdFromBytes(request.peer.id)
316
+
317
+ if ((await this.connectionGater.denyInboundRelayedConnection?.(connection.remotePeer, remotePeerId)) === true) {
318
+ this.log.error('connection gater denied inbound relayed connection from %p', connection.remotePeer)
319
+ await pbstr.write({ type: StopMessage.Type.STATUS, status: Status.PERMISSION_DENIED }, {
320
+ signal
321
+ })
322
+ await stream.close()
323
+ return
324
+ }
325
+
326
+ this.log.trace('sending success response to %p', connection.remotePeer)
327
+ await pbstr.write({ type: StopMessage.Type.STATUS, status: Status.OK }, {
328
+ signal
329
+ })
330
+
331
+ const remoteAddr = connection.remoteAddr.encapsulate(`/p2p-circuit/p2p/${remotePeerId.toString()}`)
332
+ const localAddr = this.addressManager.getAddresses()[0]
333
+ const maConn = streamToMaConnection({
334
+ stream: pbstr.unwrap().unwrap(),
335
+ remoteAddr,
336
+ localAddr,
337
+ logger: this.logger
338
+ })
339
+
340
+ this.log('new inbound transient connection %a', maConn.remoteAddr)
341
+ await this.upgrader.upgradeInbound(maConn, {
342
+ transient: true
343
+ })
344
+ this.log('%s connection %a upgraded', 'inbound', maConn.remoteAddr)
345
+ }
346
+ }
package/src/utils.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { CodeError } from '@libp2p/interface/errors'
1
+ import { CodeError } from '@libp2p/interface'
2
2
  import { anySignal } from 'any-signal'
3
3
  import { CID } from 'multiformats/cid'
4
4
  import { sha256 } from 'multiformats/hashes/sha2'
5
5
  import { ERR_TRANSFER_LIMIT_EXCEEDED } from './constants.js'
6
6
  import type { Limit } from './pb/index.js'
7
- import type { LoggerOptions } from '@libp2p/interface'
8
- import type { Stream } from '@libp2p/interface/connection'
7
+ import type { LoggerOptions, Stream } from '@libp2p/interface'
9
8
  import type { Source } from 'it-stream-types'
10
9
  import type { Uint8ArrayList } from 'uint8arraylist'
11
10
 
@@ -40,20 +39,16 @@ export function createLimitedRelay (src: Stream, dst: Stream, abortSignal: Abort
40
39
  function abortStreams (err: Error): void {
41
40
  src.abort(err)
42
41
  dst.abort(err)
43
- clearTimeout(timeout)
44
42
  }
45
43
 
46
- const abortController = new AbortController()
47
- const signal = anySignal([abortSignal, abortController.signal])
48
-
49
- let timeout: ReturnType<typeof setTimeout> | undefined
44
+ const signals = [abortSignal]
50
45
 
51
46
  if (limit?.duration != null) {
52
- timeout = setTimeout(() => {
53
- abortController.abort()
54
- }, limit.duration)
47
+ signals.push(AbortSignal.timeout(limit.duration))
55
48
  }
56
49
 
50
+ const signal = anySignal(signals)
51
+
57
52
  let srcDstFinished = false
58
53
  let dstSrcFinished = false
59
54
 
@@ -83,7 +78,6 @@ export function createLimitedRelay (src: Stream, dst: Stream, abortSignal: Abort
83
78
  if (dstSrcFinished) {
84
79
  signal.removeEventListener('abort', onAbort)
85
80
  signal.clear()
86
- clearTimeout(timeout)
87
81
  }
88
82
  })
89
83
  })
@@ -106,7 +100,6 @@ export function createLimitedRelay (src: Stream, dst: Stream, abortSignal: Abort
106
100
  if (srcDstFinished) {
107
101
  signal.removeEventListener('abort', onAbort)
108
102
  signal.clear()
109
- clearTimeout(timeout)
110
103
  }
111
104
  })
112
105
  })