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

Commit

Permalink
feat: add server.maxConnections option (#213)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
dapplion and achingbrain authored Oct 11, 2022
1 parent 5dea7d3 commit 99e88a4
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export interface TCPOptions {
* When closing a socket, wait this long for it to close gracefully before it is closed more forcibly
*/
socketCloseTimeout?: number

/**
* Set this property to reject connections when the server's connection count gets high.
* https://nodejs.org/api/net.html#servermaxconnections
*/
maxConnections?: number
}

/**
Expand Down Expand Up @@ -158,6 +164,7 @@ export class TCP implements Transport {
createListener (options: TCPCreateListenerOptions): Listener {
return new TCPListener({
...options,
maxConnections: this.opts.maxConnections,
socketInactivityTimeout: this.opts.inboundSocketInactivityTimeout,
socketCloseTimeout: this.opts.socketCloseTimeout
})
Expand Down
8 changes: 8 additions & 0 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface Context extends TCPCreateListenerOptions {
upgrader: Upgrader
socketInactivityTimeout?: number
socketCloseTimeout?: number
maxConnections?: number
}

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

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

// 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
if (context.maxConnections !== undefined) {
this.server.maxConnections = context.maxConnections
}

this.server
.on('listening', () => this.dispatchEvent(new CustomEvent('listening')))
.on('error', err => this.dispatchEvent(new CustomEvent<Error>('error', { detail: err })))
Expand Down
79 changes: 79 additions & 0 deletions test/max-connections.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from 'aegir/chai'
import net from 'node:net'
import { promisify } from 'node:util'
import { mockUpgrader } from '@libp2p/interface-mocks'
import { multiaddr } from '@multiformats/multiaddr'
import { TCP } from '../src/index.js'

describe('maxConnections', () => {
const afterEachCallbacks: Array<() => Promise<any> | any> = []
afterEach(async () => {
await Promise.all(afterEachCallbacks.map(fn => fn()))
afterEachCallbacks.length = 0
})

it('reject dial of connection above maxConnections', async () => {
const maxConnections = 2
const socketCount = 4
const port = 9900

const seenRemoteConnections = new Set<string>()
const tcp = new TCP({ maxConnections })

const upgrader = mockUpgrader()
const listener = tcp.createListener({ upgrader })
// eslint-disable-next-line @typescript-eslint/promise-function-async
afterEachCallbacks.push(() => listener.close())
await listener.listen(multiaddr(`/ip4/127.0.0.1/tcp/${port}`))

listener.addEventListener('connection', (conn) => {
seenRemoteConnections.add(conn.detail.remoteAddr.toString())
})

const sockets: net.Socket[] = []

for (let i = 0; i < socketCount; i++) {
const socket = net.connect({ port })
sockets.push(socket)

// eslint-disable-next-line @typescript-eslint/promise-function-async
afterEachCallbacks.unshift(async () => {
if (!socket.destroyed) {
socket.destroy()
await new Promise((resolve) => socket.on('close', resolve))
}
})

// Wait for connection so the order of sockets is stable, sockets expected to be alive are always [0,1]
await new Promise<void>((resolve, reject) => {
socket.on('connect', () => {
resolve()
})
socket.on('error', (err) => {
reject(err)
})
})
}

// With server.maxConnections the TCP socket is created and the initial handshake is completed
// Then in the server handler NodeJS javascript code will call socket.emit('drop') if over the limit
// https://github.com/nodejs/node/blob/fddc701d3c0eb4520f2af570876cc987ae6b4ba2/lib/net.js#L1706

// Wait for some time for server to drop all sockets above limit
await promisify(setTimeout)(250)

expect(seenRemoteConnections.size).equals(maxConnections, 'wrong serverConnections')

for (let i = 0; i < socketCount; i++) {
const socket = sockets[i]

if (i < maxConnections) {
// Assert socket connected
expect(socket.destroyed).equals(false, `socket ${i} under limit must not be destroyed`)
} else {
// Assert socket ended
expect(socket.destroyed).equals(true, `socket ${i} above limit must be destroyed`)
}
}
})
})

0 comments on commit 99e88a4

Please sign in to comment.