Skip to content
This repository was archived by the owner on Aug 29, 2023. It is now read-only.

Commit 99e88a4

Browse files
feat: add server.maxConnections option (#213)
https://nodejs.org/api/net.html#servermaxconnections If set reject connections when the server's connection count gets high Useful to prevent too resource exhaustion via many open connections on high bursts of activity Co-authored-by: achingbrain <alex@achingbrain.net>
1 parent 5dea7d3 commit 99e88a4

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export interface TCPOptions {
2929
* When closing a socket, wait this long for it to close gracefully before it is closed more forcibly
3030
*/
3131
socketCloseTimeout?: number
32+
33+
/**
34+
* Set this property to reject connections when the server's connection count gets high.
35+
* https://nodejs.org/api/net.html#servermaxconnections
36+
*/
37+
maxConnections?: number
3238
}
3339

3440
/**
@@ -158,6 +164,7 @@ export class TCP implements Transport {
158164
createListener (options: TCPCreateListenerOptions): Listener {
159165
return new TCPListener({
160166
...options,
167+
maxConnections: this.opts.maxConnections,
161168
socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout,
162169
socketCloseTimeout: this.opts.socketCloseTimeout
163170
})

src/listener.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ interface Context extends TCPCreateListenerOptions {
3030
upgrader: Upgrader
3131
socketInactivityTimeout?: number
3232
socketCloseTimeout?: number
33+
maxConnections?: number
3334
}
3435

3536
type Status = {started: false} | {started: true, listeningAddr: Multiaddr, peerId: string | null }
@@ -48,6 +49,13 @@ export class TCPListener extends EventEmitter<ListenerEvents> implements Listene
4849

4950
this.server = net.createServer(context, this.onSocket.bind(this))
5051

52+
// https://nodejs.org/api/net.html#servermaxconnections
53+
// If set reject connections when the server's connection count gets high
54+
// Useful to prevent too resource exhaustion via many open connections on high bursts of activity
55+
if (context.maxConnections !== undefined) {
56+
this.server.maxConnections = context.maxConnections
57+
}
58+
5159
this.server
5260
.on('listening', () => this.dispatchEvent(new CustomEvent('listening')))
5361
.on('error', err => this.dispatchEvent(new CustomEvent<Error>('error', { detail: err })))

test/max-connections.spec.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { expect } from 'aegir/chai'
2+
import net from 'node:net'
3+
import { promisify } from 'node:util'
4+
import { mockUpgrader } from '@libp2p/interface-mocks'
5+
import { multiaddr } from '@multiformats/multiaddr'
6+
import { TCP } from '../src/index.js'
7+
8+
describe('maxConnections', () => {
9+
const afterEachCallbacks: Array<() => Promise<any> | any> = []
10+
afterEach(async () => {
11+
await Promise.all(afterEachCallbacks.map(fn => fn()))
12+
afterEachCallbacks.length = 0
13+
})
14+
15+
it('reject dial of connection above maxConnections', async () => {
16+
const maxConnections = 2
17+
const socketCount = 4
18+
const port = 9900
19+
20+
const seenRemoteConnections = new Set<string>()
21+
const tcp = new TCP({ maxConnections })
22+
23+
const upgrader = mockUpgrader()
24+
const listener = tcp.createListener({ upgrader })
25+
// eslint-disable-next-line @typescript-eslint/promise-function-async
26+
afterEachCallbacks.push(() => listener.close())
27+
await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`))
28+
29+
listener.addEventListener('connection', (conn) => {
30+
seenRemoteConnections.add(conn.detail.remoteAddr.toString())
31+
})
32+
33+
const sockets: net.Socket[] = []
34+
35+
for (let i = 0; i < socketCount; i++) {
36+
const socket = net.connect({ port })
37+
sockets.push(socket)
38+
39+
// eslint-disable-next-line @typescript-eslint/promise-function-async
40+
afterEachCallbacks.unshift(async () => {
41+
if (!socket.destroyed) {
42+
socket.destroy()
43+
await new Promise((resolve) => socket.on('close', resolve))
44+
}
45+
})
46+
47+
// Wait for connection so the order of sockets is stable, sockets expected to be alive are always [0,1]
48+
await new Promise<void>((resolve, reject) => {
49+
socket.on('connect', () => {
50+
resolve()
51+
})
52+
socket.on('error', (err) => {
53+
reject(err)
54+
})
55+
})
56+
}
57+
58+
// With server.maxConnections the TCP socket is created and the initial handshake is completed
59+
// Then in the server handler NodeJS javascript code will call socket.emit('drop') if over the limit
60+
// https://github.com/nodejs/node/blob/fddc701d3c0eb4520f2af570876cc987ae6b4ba2/lib/net.js#L1706
61+
62+
// Wait for some time for server to drop all sockets above limit
63+
await promisify(setTimeout)(250)
64+
65+
expect(seenRemoteConnections.size).equals(maxConnections, 'wrong serverConnections')
66+
67+
for (let i = 0; i < socketCount; i++) {
68+
const socket = sockets[i]
69+
70+
if (i < maxConnections) {
71+
// Assert socket connected
72+
expect(socket.destroyed).equals(false, `socket ${i} under limit must not be destroyed`)
73+
} else {
74+
// Assert socket ended
75+
expect(socket.destroyed).equals(true, `socket ${i} above limit must be destroyed`)
76+
}
77+
}
78+
})
79+
})

0 commit comments

Comments
 (0)