@libp2p/circuit-relay-v2 0.0.0-6625a27fc → 0.0.0-6b6ba9ab7

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