Skip to content

Commit 7e4e6bd

Browse files
authoredSep 27, 2024
fix: check for connection status before storing (#2732)
It's possible that the remote can close the connection very shortly after it is opened, so check the connection status before adding it to the list of connections. Fixes a memory leak whereby the connection is already closed and then is never removed from the connections list.
1 parent a74c75d commit 7e4e6bd

File tree

7 files changed

+104
-84
lines changed

7 files changed

+104
-84
lines changed
 

‎packages/libp2p/src/connection-manager/connection-pruner.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,29 @@ export class ConnectionPruner {
4040
this.peerStore = components.peerStore
4141
this.events = components.events
4242
this.log = components.logger.forComponent('libp2p:connection-manager:connection-pruner')
43+
this.maybePruneConnections = this.maybePruneConnections.bind(this)
44+
}
4345

44-
// check the max connection limit whenever a peer connects
45-
components.events.addEventListener('connection:open', () => {
46-
this.maybePruneConnections()
47-
.catch(err => {
48-
this.log.error(err)
49-
})
50-
})
46+
start (): void {
47+
this.events.addEventListener('connection:open', this.maybePruneConnections)
48+
}
49+
50+
stop (): void {
51+
this.events.removeEventListener('connection:open', this.maybePruneConnections)
52+
}
53+
54+
maybePruneConnections (): void {
55+
this._maybePruneConnections()
56+
.catch(err => {
57+
this.log.error('error while pruning connections %e', err)
58+
})
5159
}
5260

5361
/**
5462
* If we have more connections than our maximum, select some excess connections
5563
* to prune based on peer value
5664
*/
57-
async maybePruneConnections (): Promise<void> {
65+
private async _maybePruneConnections (): Promise<void> {
5866
const connections = this.connectionManager.getConnections()
5967
const numConnections = connections.length
6068

‎packages/libp2p/src/connection-manager/dial-queue.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export class DialQueue {
153153
})
154154
})
155155

156-
if (existingConnection != null) {
156+
if (existingConnection?.status === 'open') {
157157
this.log('already connected to %a', existingConnection.remoteAddr)
158158
options.onProgress?.(new CustomProgressEvent('dial-queue:already-connected'))
159159
return existingConnection

‎packages/libp2p/src/connection-manager/index.ts

+37-38
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'
1+
import { ConnectionClosedError, InvalidMultiaddrError, InvalidParametersError, InvalidPeerIdError, NotStartedError, start, stop } from '@libp2p/interface'
22
import { PeerMap } from '@libp2p/peer-collections'
33
import { defaultAddressSort } from '@libp2p/utils/address-sort'
44
import { RateLimiter } from '@libp2p/utils/rate-limiter'
@@ -214,8 +214,6 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
214214

215215
this.onConnect = this.onConnect.bind(this)
216216
this.onDisconnect = this.onDisconnect.bind(this)
217-
this.events.addEventListener('connection:open', this.onConnect)
218-
this.events.addEventListener('connection:close', this.onDisconnect)
219217

220218
// allow/deny lists
221219
this.allow = (init.allow ?? []).map(ma => multiaddr(ma))
@@ -268,10 +266,6 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
268266

269267
readonly [Symbol.toStringTag] = '@libp2p/connection-manager'
270268

271-
isStarted (): boolean {
272-
return this.started
273-
}
274-
275269
/**
276270
* Starts the Connection Manager. If Metrics are not enabled on libp2p
277271
* only event loop and connection limits will be monitored.
@@ -288,11 +282,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
288282

289283
for (const conns of this.connections.values()) {
290284
for (const conn of conns) {
291-
if (conn.direction === 'inbound') {
292-
metric.inbound++
293-
} else {
294-
metric.outbound++
295-
}
285+
metric[conn.direction]++
296286
}
297287
}
298288

@@ -356,9 +346,13 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
356346
}
357347
})
358348

349+
this.events.addEventListener('connection:open', this.onConnect)
350+
this.events.addEventListener('connection:close', this.onDisconnect)
351+
359352
await start(
360353
this.dialQueue,
361-
this.reconnectQueue
354+
this.reconnectQueue,
355+
this.connectionPruner
362356
)
363357

364358
this.started = true
@@ -369,9 +363,13 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
369363
* Stops the Connection Manager
370364
*/
371365
async stop (): Promise<void> {
366+
this.events.removeEventListener('connection:open', this.onConnect)
367+
this.events.removeEventListener('connection:close', this.onDisconnect)
368+
372369
await stop(
373370
this.reconnectQueue,
374-
this.dialQueue
371+
this.dialQueue,
372+
this.connectionPruner
375373
)
376374

377375
// Close all connections we're tracking
@@ -413,17 +411,19 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
413411
return
414412
}
415413

416-
const peerId = connection.remotePeer
417-
const storedConns = this.connections.get(peerId)
418-
let isNewPeer = false
419-
420-
if (storedConns != null) {
421-
storedConns.push(connection)
422-
} else {
423-
isNewPeer = true
424-
this.connections.set(peerId, [connection])
414+
if (connection.status !== 'open') {
415+
// this can happen when the remote closes the connection immediately after
416+
// opening
417+
return
425418
}
426419

420+
const peerId = connection.remotePeer
421+
const isNewPeer = !this.connections.has(peerId)
422+
const storedConns = this.connections.get(peerId) ?? []
423+
storedConns.push(connection)
424+
425+
this.connections.set(peerId, storedConns)
426+
427427
// only need to store RSA public keys, all other types are embedded in the peer id
428428
if (peerId.publicKey != null && peerId.type === 'RSA') {
429429
await this.peerStore.patch(peerId, {
@@ -441,20 +441,21 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
441441
*/
442442
onDisconnect (evt: CustomEvent<Connection>): void {
443443
const { detail: connection } = evt
444+
const peerId = connection.remotePeer
445+
const peerConns = this.connections.get(peerId) ?? []
444446

445-
if (!this.started) {
446-
// This can happen when we are in the process of shutting down the node
447-
return
448-
}
447+
// remove closed connection
448+
const filteredPeerConns = peerConns.filter(conn => conn.id !== connection.id)
449449

450-
const peerId = connection.remotePeer
451-
let storedConn = this.connections.get(peerId)
450+
// update peer connections
451+
this.connections.set(peerId, filteredPeerConns)
452452

453-
if (storedConn != null && storedConn.length > 1) {
454-
storedConn = storedConn.filter((conn) => conn.id !== connection.id)
455-
this.connections.set(peerId, storedConn)
456-
} else if (storedConn != null) {
453+
if (filteredPeerConns.length === 0) {
454+
// trigger disconnect event if no connections remain
455+
this.log('onDisconnect remove all connections for peer %p', peerId)
457456
this.connections.delete(peerId)
457+
458+
// broadcast disconnect event
458459
this.events.safeDispatchEvent('peer:disconnect', { detail: connection.remotePeer })
459460
}
460461
}
@@ -478,7 +479,7 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
478479
}
479480

480481
async openConnection (peerIdOrMultiaddr: PeerId | Multiaddr | Multiaddr[], options: OpenConnectionOptions = {}): Promise<Connection> {
481-
if (!this.isStarted()) {
482+
if (!this.started) {
482483
throw new NotStartedError('Not started')
483484
}
484485

@@ -508,10 +509,8 @@ export class DefaultConnectionManager implements ConnectionManager, Startable {
508509
priority: options.priority ?? DEFAULT_DIAL_PRIORITY
509510
})
510511

511-
if (connection.remotePeer.equals(this.peerId)) {
512-
const err = new InvalidPeerIdError('Can not dial self')
513-
connection.abort(err)
514-
throw err
512+
if (connection.status !== 'open') {
513+
throw new ConnectionClosedError('Remote closed connection during opening')
515514
}
516515

517516
let peerConnections = this.connections.get(connection.remotePeer)

‎packages/libp2p/src/connection/index.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,6 @@ export class ConnectionImpl implements Connection {
161161
}
162162

163163
try {
164-
this.log.trace('closing all streams')
165-
166-
// close all streams gracefully - this can throw if we're not multiplexed
167-
await Promise.all(
168-
this.streams.map(async s => s.close(options))
169-
)
170-
171164
this.log.trace('closing underlying transport')
172165

173166
// close raw connection
@@ -184,18 +177,19 @@ export class ConnectionImpl implements Connection {
184177
}
185178

186179
abort (err: Error): void {
180+
if (this.status === 'closed') {
181+
return
182+
}
183+
187184
this.log.error('aborting connection to %a due to error', this.remoteAddr, err)
188185

189186
this.status = 'closing'
190-
this.streams.forEach(s => { s.abort(err) })
191-
192-
this.log.error('all streams aborted', this.streams.length)
193187

194188
// Abort raw connection
195189
this._abort(err)
196190

197-
this.timeline.close = Date.now()
198191
this.status = 'closed'
192+
this.timeline.close = Date.now()
199193
}
200194
}
201195

‎packages/libp2p/src/upgrader.ts

+25-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InvalidMultiaddrError, TooManyInboundProtocolStreamsError, TooManyOutboundProtocolStreamsError, LimitedConnectionError, setMaxListeners } from '@libp2p/interface'
1+
import { InvalidMultiaddrError, TooManyInboundProtocolStreamsError, TooManyOutboundProtocolStreamsError, LimitedConnectionError, setMaxListeners, InvalidPeerIdError } from '@libp2p/interface'
22
import * as mss from '@libp2p/multistream-select'
33
import { peerIdFromString } from '@libp2p/peer-id'
44
import { anySignal } from 'any-signal'
@@ -304,6 +304,14 @@ export class DefaultUpgrader implements Upgrader {
304304
remotePeer = remotePeerId
305305
}
306306

307+
// this can happen if we dial a multiaddr without a peer id, we only find
308+
// out the identity of the remote after the connection is encrypted
309+
if (remotePeer.equals(this.components.peerId)) {
310+
const err = new InvalidPeerIdError('Can not dial self')
311+
maConn.abort(err)
312+
throw err
313+
}
314+
307315
upgradedConn = encryptedConn
308316
if (opts?.muxerFactory != null) {
309317
muxerFactory = opts.muxerFactory
@@ -326,6 +334,8 @@ export class DefaultUpgrader implements Upgrader {
326334
} catch (err: any) {
327335
maConn.log.error('failed to upgrade inbound connection %s %a - %e', direction === 'inbound' ? 'from' : 'to', maConn.remoteAddr, err)
328336
throw err
337+
} finally {
338+
signal.clear()
329339
}
330340

331341
await this.shouldBlockConnection(direction === 'inbound' ? 'denyInboundUpgradedConnection' : 'denyOutboundUpgradedConnection', remotePeer, maConn)
@@ -538,22 +548,22 @@ export class DefaultUpgrader implements Upgrader {
538548
const _timeline = maConn.timeline
539549
maConn.timeline = new Proxy(_timeline, {
540550
set: (...args) => {
541-
if (connection != null && args[1] === 'close' && args[2] != null && _timeline.close == null) {
551+
if (args[1] === 'close' && args[2] != null && _timeline.close == null) {
542552
// Wait for close to finish before notifying of the closure
543553
(async () => {
544554
try {
545555
if (connection.status === 'open') {
546556
await connection.close()
547557
}
548558
} catch (err: any) {
549-
connection.log.error('error closing connection after timeline close', err)
559+
connection.log.error('error closing connection after timeline close %e', err)
550560
} finally {
551561
this.events.safeDispatchEvent('connection:close', {
552562
detail: connection
553563
})
554564
}
555565
})().catch(err => {
556-
connection.log.error('error thrown while dispatching connection:close event', err)
566+
connection.log.error('error thrown while dispatching connection:close event %e', err)
557567
})
558568
}
559569

@@ -578,32 +588,31 @@ export class DefaultUpgrader implements Upgrader {
578588
limits,
579589
logger: this.components.logger,
580590
newStream: newStream ?? errConnectionNotMultiplexed,
581-
getStreams: () => { if (muxer != null) { return muxer.streams } else { return [] } },
591+
getStreams: () => {
592+
return muxer?.streams ?? []
593+
},
582594
close: async (options?: AbortOptions) => {
583-
// Ensure remaining streams are closed gracefully
584-
if (muxer != null) {
585-
connection.log.trace('close muxer')
586-
await muxer.close(options)
587-
}
595+
// ensure remaining streams are closed gracefully
596+
await muxer?.close(options)
588597

589-
connection.log.trace('close maconn')
590598
// close the underlying transport
591599
await maConn.close(options)
592-
connection.log.trace('closed maconn')
593600
},
594601
abort: (err) => {
595602
maConn.abort(err)
596-
// Ensure remaining streams are aborted
597-
if (muxer != null) {
598-
muxer.abort(err)
599-
}
603+
604+
// ensure remaining streams are aborted
605+
muxer?.abort(err)
600606
}
601607
})
602608

603609
this.events.safeDispatchEvent('connection:open', {
604610
detail: connection
605611
})
606612

613+
// @ts-expect-error nah
614+
connection.__maConnTimeline = _timeline
615+
607616
return connection
608617
}
609618

0 commit comments

Comments
 (0)
Please sign in to comment.