Skip to content

Commit b717beb

Browse files
authored
fix: add pending connection limit (#1423)
Adds a `maxIncomingPendingConnections` option to the Connection Manager that limits how many connections can be open but not yet upgraded.
1 parent 487b942 commit b717beb

File tree

7 files changed

+81
-10
lines changed

7 files changed

+81
-10
lines changed

doc/LIMITS.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ In order to prevent excessive resource consumption by a libp2p node it's importa
2020

2121
It's possible to limit the amount of incoming and outgoing connections a node is able to make. When this limit is reached and an attempt to open a new connection is made, existing connections may be closed to make room for the new connection.
2222

23+
We can also limit the number of connections in a "pending" state. These connections have been opened by a remote peer but peer IDs have yet to be exchanged and/or connection encryption and multiplexing negotiated. Once this limit is hit further connections will be closed unless the remote peer has an address in the [allow list](#allowdeny-lists).
24+
2325
```js
2426
const node = await createLibp2pNode({
2527
connectionManager: {
@@ -32,7 +34,12 @@ const node = await createLibp2pNode({
3234
* If the number of open connections goes below this number, the node
3335
* will try to connect to nearby peers from the peer store
3436
*/
35-
minConnections: 20
37+
minConnections: 20,
38+
39+
/**
40+
* How many connections can be open but not yet upgraded
41+
*/
42+
maxIncomingPendingConnections: 10
3643
}
3744
})
3845
```

examples/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"license": "MIT",
1111
"dependencies": {
1212
"@libp2p/pubsub-peer-discovery": "^6.0.2",
13-
"@libp2p/floodsub": "^4.0.0",
13+
"@libp2p/floodsub": "^4.0.1",
1414
"@nodeutils/defaults-deep": "^1.1.0",
1515
"execa": "^6.1.0",
1616
"fs-extra": "^10.1.0",

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
"@libp2p/interface-peer-info": "^1.0.3",
114114
"@libp2p/interface-peer-routing": "^1.0.1",
115115
"@libp2p/interface-peer-store": "^1.2.2",
116-
"@libp2p/interface-pubsub": "^2.1.0",
116+
"@libp2p/interface-pubsub": "^3.0.0",
117117
"@libp2p/interface-registrar": "^2.0.3",
118118
"@libp2p/interface-stream-muxer": "^3.0.0",
119119
"@libp2p/interface-transport": "^2.0.0",
@@ -174,7 +174,7 @@
174174
"@libp2p/bootstrap": "^4.0.0",
175175
"@libp2p/daemon-client": "^3.0.1",
176176
"@libp2p/daemon-server": "^3.0.1",
177-
"@libp2p/floodsub": "^4.0.0",
177+
"@libp2p/floodsub": "^4.0.1",
178178
"@libp2p/interface-compliance-tests": "^3.0.2",
179179
"@libp2p/interface-connection-encrypter-compliance-tests": "^2.0.2",
180180
"@libp2p/interface-mocks": "^6.0.1",
@@ -211,4 +211,4 @@
211211
"browser": {
212212
"nat-api": false
213213
}
214-
}
214+
}

src/connection-manager/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const defaultOptions: Partial<ConnectionManagerInit> = {
3333
pollInterval: 2000,
3434
autoDialInterval: 10000,
3535
movingAverageInterval: 60000,
36-
inboundConnectionThreshold: 5
36+
inboundConnectionThreshold: 5,
37+
maxIncomingPendingConnections: 10
3738
}
3839

3940
const METRICS_SYSTEM = 'libp2p'
@@ -152,6 +153,12 @@ export interface ConnectionManagerInit {
152153
* host, reject subsequent connections
153154
*/
154155
inboundConnectionThreshold?: number
156+
157+
/**
158+
* The maximum number of parallel incoming connections allowed that have yet to
159+
* complete the connection upgrade - e.g. choosing connection encryption, muxer, etc
160+
*/
161+
maxIncomingPendingConnections?: number
155162
}
156163

157164
export interface ConnectionManagerEvents {
@@ -175,6 +182,7 @@ export class DefaultConnectionManager extends EventEmitter<ConnectionManagerEven
175182
private readonly allow: Multiaddr[]
176183
private readonly deny: Multiaddr[]
177184
private readonly inboundConnectionRateLimiter: RateLimiterMemory
185+
private incomingPendingConnections: number
178186

179187
constructor (init: ConnectionManagerInit) {
180188
super()
@@ -218,6 +226,8 @@ export class DefaultConnectionManager extends EventEmitter<ConnectionManagerEven
218226
points: this.opts.inboundConnectionThreshold,
219227
duration: 1
220228
})
229+
230+
this.incomingPendingConnections = 0
221231
}
222232

223233
init (components: Components): void {
@@ -719,9 +729,17 @@ export class DefaultConnectionManager extends EventEmitter<ConnectionManagerEven
719729
})
720730

721731
if (allowConnection) {
732+
this.incomingPendingConnections++
733+
722734
return true
723735
}
724736

737+
// check pending connections
738+
if (this.incomingPendingConnections === this.opts.maxIncomingPendingConnections) {
739+
log('connection from %s refused - incomingPendingConnections exceeded by peer %s', maConn.remoteAddr)
740+
return false
741+
}
742+
725743
if (maConn.remoteAddr.isThinWaistAddress()) {
726744
const host = maConn.remoteAddr.nodeAddress().address
727745

@@ -734,6 +752,8 @@ export class DefaultConnectionManager extends EventEmitter<ConnectionManagerEven
734752
}
735753

736754
if (this.getConnections().length < this.opts.maxConnections) {
755+
this.incomingPendingConnections++
756+
737757
return true
738758
}
739759

@@ -742,6 +762,6 @@ export class DefaultConnectionManager extends EventEmitter<ConnectionManagerEven
742762
}
743763

744764
afterUpgradeInbound () {
745-
765+
this.incomingPendingConnections--
746766
}
747767
}

src/pubsub/dummy-pubsub.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { EventEmitter } from '@libp2p/interfaces/events'
22
import type { PeerId } from '@libp2p/interface-peer-id'
3-
import type { PublishResult, PubSub, PubSubEvents, StrictNoSign, StrictSign } from '@libp2p/interface-pubsub'
3+
import type { PublishResult, PubSub, PubSubEvents, StrictNoSign, StrictSign, TopicValidatorFn } from '@libp2p/interface-pubsub'
44
import errCode from 'err-code'
55
import { messages, codes } from '../errors.js'
66

77
export class DummyPubSub extends EventEmitter<PubSubEvents> implements PubSub {
8+
public topicValidators = new Map<string, TopicValidatorFn>()
9+
810
isStarted (): boolean {
911
return false
1012
}

src/upgrader.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ export class DefaultUpgrader extends EventEmitter<UpgraderEvents> implements Upg
126126
const accept = await this.components.getConnectionManager().acceptIncomingConnection(maConn)
127127

128128
if (!accept) {
129-
await maConn.close()
130129
throw errCode(new Error('connection denied'), codes.ERR_CONNECTION_DENIED)
131130
}
132131

@@ -201,7 +200,6 @@ export class DefaultUpgrader extends EventEmitter<UpgraderEvents> implements Upg
201200
}
202201
} catch (err: any) {
203202
log.error('Failed to upgrade inbound connection', err)
204-
await maConn.close(err)
205203
throw err
206204
}
207205

@@ -228,6 +226,7 @@ export class DefaultUpgrader extends EventEmitter<UpgraderEvents> implements Upg
228226
remotePeer
229227
})
230228
} finally {
229+
this.components.getConnectionManager().afterUpgradeInbound()
231230
timeoutController.clear()
232231
}
233232
}

test/connection-manager/index.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,47 @@ describe('Connection Manager', () => {
304304
await expect(connectionManager.acceptIncomingConnection(maConn))
305305
.to.eventually.be.true()
306306
})
307+
308+
it('should limit the number of inbound pending connections', async () => {
309+
const connectionManager = new DefaultConnectionManager({
310+
...defaultOptions,
311+
maxIncomingPendingConnections: 1
312+
})
313+
314+
const dialer = stubInterface<Dialer>()
315+
dialer.dial.resolves(stubInterface<Connection>())
316+
317+
const components = new Components({
318+
dialer
319+
})
320+
321+
// set mocks
322+
connectionManager.init(components)
323+
324+
// start the upgrade
325+
const maConn1 = mockMultiaddrConnection({
326+
source: [],
327+
sink: async () => {}
328+
}, await createEd25519PeerId())
329+
330+
await expect(connectionManager.acceptIncomingConnection(maConn1))
331+
.to.eventually.be.true()
332+
333+
// start the upgrade
334+
const maConn2 = mockMultiaddrConnection({
335+
source: [],
336+
sink: async () => {}
337+
}, await createEd25519PeerId())
338+
339+
// should be false because we have not completed the upgrade of maConn1
340+
await expect(connectionManager.acceptIncomingConnection(maConn2))
341+
.to.eventually.be.false()
342+
343+
// finish the maConn1 pending upgrade
344+
connectionManager.afterUpgradeInbound()
345+
346+
// should be true because we have now completed the upgrade of maConn1
347+
await expect(connectionManager.acceptIncomingConnection(maConn2))
348+
.to.eventually.be.true()
349+
})
307350
})

0 commit comments

Comments
 (0)