Skip to content

Commit 5d199f9

Browse files
authored
fix: handle more circuit relay refresh failures (#2764)
Handles some more instances where we don't remove old reservations when refreshing them fails.
1 parent a2f1748 commit 5d199f9

File tree

14 files changed

+313
-147
lines changed

14 files changed

+313
-147
lines changed

packages/integration-tests/test/circuit-relay.node.ts

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -540,41 +540,9 @@ describe('circuit-relay', () => {
540540
const ma = getRelayAddress(relay1).encapsulate(`/p2p-circuit/p2p/${remote.peerId.toString()}`)
541541

542542
await expect(local.dial(ma)).to.eventually.be.rejected
543-
.with.property('name', 'DialError')
543+
.with.property('name', 'NoValidAddressesError')
544544
})
545-
/*
546-
it('should fail to open connection over relayed connection', async () => {
547-
// relay1 dials relay2
548-
await relay1.dial(relay2.getMultiaddrs()[0])
549-
await usingAsRelay(relay1, relay2)
550-
551-
// remote dials relay2
552-
await remote.dial(relay2.getMultiaddrs()[0])
553-
await usingAsRelay(remote, relay2)
554-
555-
// local dials relay1 via relay2
556-
const ma = getRelayAddress(relay1)
557545

558-
// open hop stream and try to connect to remote
559-
const stream = await local.dialProtocol(ma, RELAY_V2_HOP_CODEC, {
560-
runOnLimitedConnection: true
561-
})
562-
563-
const hopStream = pbStream(stream).pb(HopMessage)
564-
565-
await hopStream.write({
566-
type: HopMessage.Type.CONNECT,
567-
peer: {
568-
id: remote.peerId.toMultihash().bytes,
569-
addrs: []
570-
}
571-
})
572-
573-
const response = await hopStream.read()
574-
expect(response).to.have.property('type', HopMessage.Type.STATUS)
575-
expect(response).to.have.property('status', Status.PERMISSION_DENIED)
576-
})
577-
*/
578546
it('should emit connection:close when relay stops', async () => {
579547
// discover relay and make reservation
580548
await remote.dial(relay1.getMultiaddrs()[0])

packages/transport-circuit-relay-v2/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,27 @@
5252
"doc-check": "aegir doc-check"
5353
},
5454
"dependencies": {
55+
"@libp2p/crypto": "^5.0.5",
5556
"@libp2p/interface": "^2.1.3",
5657
"@libp2p/interface-internal": "^2.0.8",
5758
"@libp2p/peer-collections": "^6.0.8",
5859
"@libp2p/peer-id": "^5.0.5",
5960
"@libp2p/peer-record": "^8.0.8",
6061
"@libp2p/utils": "^6.1.1",
61-
"@multiformats/mafmt": "^12.1.6",
6262
"@multiformats/multiaddr": "^12.2.3",
63+
"@multiformats/multiaddr-matcher": "^1.3.0",
6364
"any-signal": "^4.1.1",
6465
"it-protobuf-stream": "^1.1.3",
6566
"it-stream-types": "^2.0.1",
6667
"multiformats": "^13.1.0",
6768
"progress-events": "^1.0.0",
6869
"protons-runtime": "^5.4.0",
6970
"race-signal": "^1.0.2",
71+
"retimeable-signal": "^0.0.0",
7072
"uint8arraylist": "^2.4.8",
7173
"uint8arrays": "^5.1.0"
7274
},
7375
"devDependencies": {
74-
"@libp2p/crypto": "^5.0.5",
7576
"@libp2p/interface-compliance-tests": "^6.1.6",
7677
"@libp2p/logger": "^5.1.1",
7778
"aegir": "^44.0.1",

packages/transport-circuit-relay-v2/src/constants.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ export const CIRCUIT_PROTO_CODE = 290
1313
*/
1414
export const DEFAULT_MAX_RESERVATION_STORE_SIZE = 15
1515

16-
/**
17-
* How often to check for reservation expiry
18-
*/
19-
export const DEFAULT_MAX_RESERVATION_CLEAR_INTERVAL = 300 * second
20-
2116
/**
2217
* How often to check for reservation expiry
2318
*/

packages/transport-circuit-relay-v2/src/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,31 @@ import type { Limit } from './pb/index.js'
4343
import type { TypedEventEmitter } from '@libp2p/interface'
4444
import type { PeerMap } from '@libp2p/peer-collections'
4545
import type { Multiaddr } from '@multiformats/multiaddr'
46+
import type { RetimeableAbortSignal } from 'retimeable-signal'
4647

4748
export type { Limit }
4849

4950
export interface RelayReservation {
50-
expire: Date
51+
/**
52+
* When this reservation expires
53+
*/
54+
expiry: Date
55+
56+
/**
57+
* The address of the relay client
58+
*/
5159
addr: Multiaddr
60+
61+
/**
62+
* How much data can be transferred over each relayed connection and for how
63+
* long before the underlying stream is reset
64+
*/
5265
limit?: Limit
66+
67+
/**
68+
* This signal will fire it's "abort" event when the reservation expires
69+
*/
70+
signal: RetimeableAbortSignal
5371
}
5472

5573
export interface CircuitRelayServiceEvents {

packages/transport-circuit-relay-v2/src/pb/index.proto

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ message Peer {
4040
message Reservation {
4141
uint64 expire = 1; // Unix expiration time (UTC)
4242
repeated bytes addrs = 2; // relay addrs for reserving peer
43-
optional bytes voucher = 3; // reservation voucher
43+
optional Envelope voucher = 3; // reservation voucher
4444
}
4545

4646
message Limit {
@@ -65,3 +65,22 @@ message ReservationVoucher {
6565
bytes peer = 2;
6666
uint64 expiration = 3;
6767
}
68+
69+
// https://github.com/libp2p/specs/blob/master/RFC/0002-signed-envelopes.md
70+
message Envelope {
71+
// public_key is the public key of the keypair the enclosed payload was
72+
// signed with.
73+
bytes public_key = 1;
74+
75+
// payload_type encodes the type of payload, so that it can be deserialized
76+
// deterministically.
77+
bytes payload_type = 2;
78+
79+
// payload is the actual payload carried inside this envelope.
80+
ReservationVoucher payload = 3;
81+
82+
// signature is the signature produced by the private key corresponding to
83+
// the enclosed public key, over the payload, prefixing a domain string for
84+
// additional security.
85+
bytes signature = 5;
86+
}

packages/transport-circuit-relay-v2/src/pb/index.ts

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export namespace Peer {
318318
export interface Reservation {
319319
expire: bigint
320320
addrs: Uint8Array[]
321-
voucher?: Uint8Array
321+
voucher?: Envelope
322322
}
323323

324324
export namespace Reservation {
@@ -345,7 +345,7 @@ export namespace Reservation {
345345

346346
if (obj.voucher != null) {
347347
w.uint32(26)
348-
w.bytes(obj.voucher)
348+
Envelope.codec().encode(obj.voucher, w)
349349
}
350350

351351
if (opts.lengthDelimited !== false) {
@@ -376,7 +376,9 @@ export namespace Reservation {
376376
break
377377
}
378378
case 3: {
379-
obj.voucher = reader.bytes()
379+
obj.voucher = Envelope.codec().decode(reader, reader.uint32(), {
380+
limits: opts.limits?.voucher
381+
})
380382
break
381383
}
382384
default: {
@@ -580,3 +582,97 @@ export namespace ReservationVoucher {
580582
return decodeMessage(buf, ReservationVoucher.codec(), opts)
581583
}
582584
}
585+
586+
export interface Envelope {
587+
publicKey: Uint8Array
588+
payloadType: Uint8Array
589+
payload?: ReservationVoucher
590+
signature: Uint8Array
591+
}
592+
593+
export namespace Envelope {
594+
let _codec: Codec<Envelope>
595+
596+
export const codec = (): Codec<Envelope> => {
597+
if (_codec == null) {
598+
_codec = message<Envelope>((obj, w, opts = {}) => {
599+
if (opts.lengthDelimited !== false) {
600+
w.fork()
601+
}
602+
603+
if ((obj.publicKey != null && obj.publicKey.byteLength > 0)) {
604+
w.uint32(10)
605+
w.bytes(obj.publicKey)
606+
}
607+
608+
if ((obj.payloadType != null && obj.payloadType.byteLength > 0)) {
609+
w.uint32(18)
610+
w.bytes(obj.payloadType)
611+
}
612+
613+
if (obj.payload != null) {
614+
w.uint32(26)
615+
ReservationVoucher.codec().encode(obj.payload, w)
616+
}
617+
618+
if ((obj.signature != null && obj.signature.byteLength > 0)) {
619+
w.uint32(42)
620+
w.bytes(obj.signature)
621+
}
622+
623+
if (opts.lengthDelimited !== false) {
624+
w.ldelim()
625+
}
626+
}, (reader, length, opts = {}) => {
627+
const obj: any = {
628+
publicKey: uint8ArrayAlloc(0),
629+
payloadType: uint8ArrayAlloc(0),
630+
signature: uint8ArrayAlloc(0)
631+
}
632+
633+
const end = length == null ? reader.len : reader.pos + length
634+
635+
while (reader.pos < end) {
636+
const tag = reader.uint32()
637+
638+
switch (tag >>> 3) {
639+
case 1: {
640+
obj.publicKey = reader.bytes()
641+
break
642+
}
643+
case 2: {
644+
obj.payloadType = reader.bytes()
645+
break
646+
}
647+
case 3: {
648+
obj.payload = ReservationVoucher.codec().decode(reader, reader.uint32(), {
649+
limits: opts.limits?.payload
650+
})
651+
break
652+
}
653+
case 5: {
654+
obj.signature = reader.bytes()
655+
break
656+
}
657+
default: {
658+
reader.skipType(tag & 7)
659+
break
660+
}
661+
}
662+
}
663+
664+
return obj
665+
})
666+
}
667+
668+
return _codec
669+
}
670+
671+
export const encode = (obj: Partial<Envelope>): Uint8Array => {
672+
return encodeMessage(obj, Envelope.codec())
673+
}
674+
675+
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<Envelope>): Envelope => {
676+
return decodeMessage(buf, Envelope.codec(), opts)
677+
}
678+
}

packages/transport-circuit-relay-v2/src/server/index.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { publicKeyToProtobuf } from '@libp2p/crypto/keys'
12
import { TypedEventEmitter, setMaxListeners } from '@libp2p/interface'
23
import { peerIdFromMultihash } from '@libp2p/peer-id'
34
import { RecordEnvelope } from '@libp2p/peer-record'
@@ -156,16 +157,14 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
156157
runOnLimitedConnection: true
157158
})
158159

159-
this.reservationStore.start()
160-
161160
this.started = true
162161
}
163162

164163
/**
165164
* Stop Relay service
166165
*/
167166
async stop (): Promise<void> {
168-
this.reservationStore.stop()
167+
this.reservationStore.clear()
169168
this.shutdownController.abort()
170169
await this.registrar.unhandle(RELAY_V2_HOP_CODEC)
171170

@@ -290,16 +289,25 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
290289
addrs.push(relayAddr.bytes)
291290
}
292291

293-
const voucher = await RecordEnvelope.seal(new ReservationVoucherRecord({
292+
const envelope = await RecordEnvelope.seal(new ReservationVoucherRecord({
294293
peer: remotePeer,
295294
relay: this.peerId,
296-
expiration: Number(expire)
295+
expiration: expire
297296
}), this.privateKey)
298297

299298
return {
300299
addrs,
301300
expire,
302-
voucher: voucher.marshal()
301+
voucher: {
302+
publicKey: publicKeyToProtobuf(envelope.publicKey),
303+
payloadType: envelope.payloadType,
304+
payload: {
305+
peer: remotePeer.toMultihash().bytes,
306+
relay: this.peerId.toMultihash().bytes,
307+
expiration: expire
308+
},
309+
signature: envelope.signature
310+
}
303311
}
304312
}
305313

@@ -330,7 +338,9 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
330338
return
331339
}
332340

333-
if (!this.reservationStore.hasReservation(dstPeer)) {
341+
const reservation = this.reservationStore.get(dstPeer)
342+
343+
if (reservation == null) {
334344
this.log.error('hop connect denied for destination peer %p not having a reservation for %p with status %s', dstPeer, connection.remotePeer, Status.NO_RESERVATION)
335345
await hopstr.write({ type: HopMessage.Type.STATUS, status: Status.NO_RESERVATION }, options)
336346
return
@@ -350,7 +360,6 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
350360
return
351361
}
352362

353-
const limit = this.reservationStore.get(dstPeer)?.limit
354363
const destinationConnection = connections[0]
355364

356365
const destinationStream = await this.stopHop({
@@ -361,7 +370,7 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
361370
id: connection.remotePeer.toMultihash().bytes,
362371
addrs: []
363372
},
364-
limit
373+
limit: reservation?.limit
365374
}
366375
}, options)
367376

@@ -374,13 +383,14 @@ class CircuitRelayServer extends TypedEventEmitter<RelayServerEvents> implements
374383
await hopstr.write({
375384
type: HopMessage.Type.STATUS,
376385
status: Status.OK,
377-
limit
386+
limit: reservation?.limit
378387
}, options)
379388
const sourceStream = stream.unwrap()
380389

381390
this.log('connection from %p to %p established - merging streams', connection.remotePeer, dstPeer)
391+
382392
// Short circuit the two streams to create the relayed connection
383-
createLimitedRelay(sourceStream, destinationStream, this.shutdownController.signal, limit, {
393+
createLimitedRelay(sourceStream, destinationStream, this.shutdownController.signal, reservation, {
384394
log: this.log
385395
})
386396
}

0 commit comments

Comments
 (0)