From 4456e98e3f6268eca026b3ad7df0156ae99d7fec Mon Sep 17 00:00:00 2001 From: Joshua Thomas Date: Mon, 16 Mar 2026 19:41:08 -0700 Subject: [PATCH 1/2] Update .gitignore and enhance SocksServer configuration - Added .vscode to .gitignore to exclude Visual Studio Code settings from version control. - Updated SocksServer class to include localPort option for net.createConnection, further improving flexibility in connection handling. --- .gitignore | 1 + src/socks5.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b8d6a18..5ab63cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .nyc_output +.vscode dist npm-debug.log lib-cov diff --git a/src/socks5.js b/src/socks5.js index 5481c2a..94a4575 100644 --- a/src/socks5.js +++ b/src/socks5.js @@ -318,8 +318,9 @@ class SocksServer { const destination = net.createConnection( { host: args.dst.addr, - port: args.dst.port, localAddress: self.options.localAddress, + localPort: self.options.localPort, + port: args.dst.port, }, () => { // prepare a success response From 1f2e8c2079ded472704573a34df69cdfdb0b1d57 Mon Sep 17 00:00:00 2001 From: Joshua Thomas Date: Mon, 16 Mar 2026 19:58:41 -0700 Subject: [PATCH 2/2] Update version to 3.5.0 and enhance SocksServer functionality - Bumped package version to 3.5.0. - Added `connectTimeout` and `destinationIdleTimeout` options to the SocksServer for improved connection handling. - Updated README to document new options. - Added tests for `destinationIdleTimeout` and `connectTimeout` functionality. --- package.json | 5 +-- readme.md | 2 ++ src/socks5.js | 56 +++++++++++++++++++++++-------- test/index.js | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index d72358c..bb9cc22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-socks", - "version": "3.4.0", + "version": "3.5.0", "description": "proxies requests ", "type": "module", "main": "src/socks5.js", @@ -19,7 +19,8 @@ "contributors": [ "https://github.com/pronskiy", "https://github.com/fabiensk", - "https://github.com/lanius-collaris" + "https://github.com/lanius-collaris", + "https://github.com/adsr" ], "license": "MIT", "keywords": [ diff --git a/readme.md b/readme.md index 9fdf3c7..8d8c045 100644 --- a/readme.md +++ b/readme.md @@ -179,7 +179,9 @@ This method accepts an optional `options` argument: - `options.authentication` - A callback for authentication - `options.connectionFilter` - A callback for connection filtering +- `options.connectTimeout` - Milliseconds to wait for destination connect phase before failing the request (0 is disabled, default 0) - `options.idleTimeout` - Milliseconds of inactivity before destroying client/destination sockets (0 is disabled, default 0) +- `options.destinationIdleTimeout` - Milliseconds of inactivity before destroying destination sockets; falls back to `options.idleTimeout` when not explicitly set - `options.compatAuth` - Non-default RFC 1929 compatibility controls for empty credentials (defaults are strict) #### authentication callback diff --git a/src/socks5.js b/src/socks5.js index d05b720..aa69ae5 100644 --- a/src/socks5.js +++ b/src/socks5.js @@ -48,6 +48,14 @@ class SocksServer { } this.idleTimeout = this.options.idleTimeout || 0; + this.connectTimeout = this.options.connectTimeout || 0; + this.destinationIdleTimeout = Object.prototype.hasOwnProperty.call( + this.options, + 'destinationIdleTimeout', + ) + ? this.options.destinationIdleTimeout + : this.idleTimeout; + this.server = net.createServer((socket) => { socket.on('error', (err) => { self.server.emit(EVENTS.PROXY_ERROR, err); @@ -338,18 +346,21 @@ class SocksServer { // configure idle timeout for destination socket if ( - self.idleTimeout + self.destinationIdleTimeout && typeof destination.setTimeout === 'function' ) { - destination.setTimeout(self.idleTimeout, () => { - try { - destination.destroy( - new Error('destination idle timeout'), - ); - } catch { - // ignore errors - } - }); + destination.setTimeout( + self.destinationIdleTimeout, + () => { + try { + destination.destroy( + new Error('destination idle timeout'), + ); + } catch { + // ignore socket destroy errors + } + }, + ); } // ensure proper teardown when either side ends/closes/errors @@ -437,10 +448,27 @@ class SocksServer { return end(RFC_1928_REPLIES.NETWORK_UNREACHABLE, args); }); - if (self.options.connTimeout) { - destination.setTimeout(self.options.connTimeout); - destination.on('timeout', () => { - destination.destroy(); + if ( + self.connectTimeout + && typeof destination.setTimeout === 'function' + ) { + const onConnectTimeout = () => { + const timeoutError = new Error('destination connect timeout'); + timeoutError.code = 'ETIMEDOUT'; + try { + destination.destroy(timeoutError); + } catch { + // ignore socket destroy errors + } + }; + + destination.setTimeout(self.connectTimeout); + destination.once('timeout', onConnectTimeout); + destination.once('connect', () => { + destination.off('timeout', onConnectTimeout); + if (!self.destinationIdleTimeout) { + destination.setTimeout(0); + } }); } }), diff --git a/test/index.js b/test/index.js index 4d41930..19580b5 100644 --- a/test/index.js +++ b/test/index.js @@ -59,6 +59,36 @@ function createMockDestinationSocket(error) { return destination; } +function createMockPendingDestinationSocket() { + const destination = new EventEmitter(); + let timeoutId; + destination.pipe = () => destination; + destination.setTimeout = (ms) => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = undefined; + } + if (ms > 0) { + timeoutId = setTimeout(() => { + destination.emit('timeout'); + }, ms); + } + return destination; + }; + destination.destroy = (err) => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = undefined; + } + if (err) { + setImmediate(() => destination.emit('error', err)); + } + setImmediate(() => destination.emit('close', Boolean(err))); + return destination; + }; + return destination; +} + await test('no-auth: connect to local echo', async () => { const echo = await createEchoServer(); const app = socks5.createServer(); @@ -186,6 +216,69 @@ await test('activeSessions returns to 0 after idle timeout', async () => { await closeServer(echo.server); }); +await test('destinationIdleTimeout destroys idle destination socket', async () => { + const echo = await createEchoServer(); + const app = socks5.createServer({ destinationIdleTimeout: 50 }); + await listenServer(app); + const addr = app.address(); + + const client = await connectTo(addr.port, addr.address); + client.write(buildSocks5Handshake(0x00)); + const selection = await readExactly(client, 2); + assert.strictEqual(selection[0], 0x05); + assert.strictEqual(selection[1], 0x00); + + client.write(buildSocks5ConnectRequest(echo.host, echo.port)); + const connectResponse = await readExactly(client, 2); + assert.strictEqual(connectResponse[0], 0x05); + assert.strictEqual(connectResponse[1], 0x00); + + const disconnect = await Promise.race([ + once(app, 'proxyDisconnect'), + new Promise((_, reject) => + setTimeout( + () => reject(new Error('expected proxyDisconnect from destinationIdleTimeout')), + 500, + ) + ), + ]); + assert.ok(disconnect); + + client.destroy(); + await closeServer(app); + await closeServer(echo.server); +}); + +await test('connectTimeout fails destination connection when connect phase is too slow', async () => { + const app = socks5.createServer({ connectTimeout: 25, destinationIdleTimeout: 0 }); + await listenServer(app); + const addr = app.address(); + const client = await connectTo(addr.port, addr.address); + + client.write(buildSocks5Handshake(0x00)); + const selection = await readExactly(client, 2); + assert.strictEqual(selection[0], 0x05); + assert.strictEqual(selection[1], 0x00); + + const originalCreateConnection = net.createConnection; + try { + net.createConnection = () => createMockPendingDestinationSocket(); + + const proxyErrorPromise = once(app, 'proxyError'); + client.write(buildSocks5ConnectRequest('127.0.0.1', 80)); + const response = await readExactly(client, 2); + assert.strictEqual(response[0], 0x05); + assert.strictEqual(response[1], 0x03); + + const proxyError = await proxyErrorPromise; + assert.strictEqual(proxyError.code, 'ETIMEDOUT'); + } finally { + net.createConnection = originalCreateConnection; + client.destroy(); + await closeServer(app); + } +}); + await test('invalid handshake version returns general failure', async () => { const app = socks5.createServer(); await listenServer(app);