From a6717aab92e668b00d81704a28b8cf88c664b502 Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Mon, 27 Apr 2026 23:35:10 +0200 Subject: [PATCH] feat: intercept and forward to our own destination --- index.d.ts | 3 +- src/socks5.js | 266 ++++++++++++++++++++++++++++---------------------- 2 files changed, 151 insertions(+), 118 deletions(-) diff --git a/index.d.ts b/index.d.ts index 07b9c8c..4c5683c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,5 @@ import { Server, Socket } from 'net'; +import * as stream from 'stream'; export interface DestinationInfo { address: string; @@ -11,7 +12,7 @@ export interface OriginInfo { } export type AuthenticateCallback = (err?: Error) => void; -export type ConnectionFilterCallback = (err?: Error) => void; +export type ConnectionFilterCallback = (err?: Error, dest?: stream.Duplex) => void; export type AuthenticateFn = ( username: string, diff --git a/src/socks5.js b/src/socks5.js index aa69ae5..27be126 100644 --- a/src/socks5.js +++ b/src/socks5.js @@ -322,70 +322,24 @@ class SocksServer { address: socket.remoteAddress, port: socket.remotePort, }, - connectionFilterDomain.intercept(() => { - const destination = net.createConnection( - { - host: args.dst.addr, - localAddress: self.options.localAddress, - localPort: self.options.localPort, - port: args.dst.port, - }, - () => { - // prepare a success response - const responseBuffer = Buffer.alloc( - args.requestBuffer.length, - ); - args.requestBuffer.copy(responseBuffer); - responseBuffer[1] = RFC_1928_REPLIES.SUCCEEDED; - - // write acknowledgement to client... - socket.write(responseBuffer, () => { - // listen for data bi-directionally - destination.pipe(socket); - socket.pipe(destination); - - // configure idle timeout for destination socket - if ( - self.destinationIdleTimeout - && typeof destination.setTimeout === 'function' - ) { - destination.setTimeout( - self.destinationIdleTimeout, - () => { - try { - destination.destroy( - new Error('destination idle timeout'), - ); - } catch { - // ignore socket destroy errors - } - }, - ); - } + connectionFilterDomain.intercept((destination) => { + const sendSuccessResponse = (dest, callback) => { + // prepare a success response + const responseBuffer = Buffer.alloc( + args.requestBuffer.length, + ); + args.requestBuffer.copy(responseBuffer); + responseBuffer[1] = RFC_1928_REPLIES.SUCCEEDED; - // ensure proper teardown when either side ends/closes/errors - const teardownDestination = () => { - try { - destination.destroy(); - } catch { - // ignore socket destroy errors - } - }; - const teardownSocket = () => { - try { - socket.destroy(); - } catch { - // ignore socket destroy errors - } - }; - - socket.once('close', teardownDestination); - socket.once('end', teardownDestination); - socket.once('error', teardownDestination); - destination.once('error', teardownSocket); - }); - }, - ); + // write acknowledgement to client... + socket.write(responseBuffer, () => { + // listen for data bi-directionally + dest.pipe(socket); + socket.pipe(dest); + + callback && callback(dest); + }); + }; const destinationInfo = { address: args.dst.addr, @@ -396,80 +350,158 @@ class SocksServer { port: socket.remotePort, }; - // capture successful connection - destination.on('connect', () => { - // emit connection event - self.server.emit( - EVENTS.PROXY_CONNECT, - destinationInfo, - destination, - ); + if (destination) { + // exit the connection filter domain + connectionFilterDomain.exit(); - // capture and emit proxied connection data - destination.on('data', (data) => { - self.server.emit(EVENTS.PROXY_DATA, data); - }); + sendSuccessResponse(destination); // capture close of destination and emit pending disconnect // note: this event is only emitted once the destination socket is fully closed destination.on('close', (hadError) => { // indicate client connection end + self.server.emit(EVENTS.PROXY_DISCONNECT, originInfo, destinationInfo, hadError); + }); + + // capture connection errors and response appropriately + destination.on('error', (err) => { + // notify of connection error + err.addr = args.dst.addr; + err.atyp = args.atyp; + err.port = args.dst.port; + + self.server.emit(EVENTS.PROXY_ERROR, err); + + return end(RFC_1928_REPLIES.NETWORK_UNREACHABLE, args); + }); + } else { + const onWrittenResponse = (dest) => { + // configure idle timeout for destination socket + if ( + self.destinationIdleTimeout + && typeof destination.setTimeout === 'function' + ) { + dest.setTimeout( + self.destinationIdleTimeout, + () => { + try { + dest.destroy( + new Error('destination idle timeout'), + ); + } catch { + // ignore socket destroy errors + } + }, + ); + } + + // ensure proper teardown when either side ends/closes/errors + const teardownDestination = () => { + try { + dest.destroy(); + } catch { + // ignore socket destroy errors + } + }; + const teardownSocket = () => { + try { + socket.destroy(); + } catch { + // ignore socket destroy errors + } + }; + + socket.once('close', teardownDestination); + socket.once('end', teardownDestination); + socket.once('error', teardownDestination); + dest.once('error', teardownSocket); + }; + + const destination = net.createConnection( + { + host: args.dst.addr, + localAddress: self.options.localAddress, + localPort: self.options.localPort, + port: args.dst.port, + }, + () => sendSuccessResponse(destination, onWrittenResponse), + ); + + // capture successful connection + destination.on('connect', () => { + // emit connection event self.server.emit( - EVENTS.PROXY_DISCONNECT, - originInfo, + EVENTS.PROXY_CONNECT, destinationInfo, - hadError, + destination, ); - }); - connectionFilterDomain.exit(); - }); - - // capture connection errors and response appropriately - destination.on('error', (err) => { - // exit the connection filter domain - connectionFilterDomain.exit(); + // capture and emit proxied connection data + destination.on('data', (data) => { + self.server.emit(EVENTS.PROXY_DATA, data); + }); - // notify of connection error - err.addr = args.dst.addr; - err.atyp = args.atyp; - err.port = args.dst.port; + // capture close of destination and emit pending disconnect + // note: this event is only emitted once the destination socket is fully closed + destination.on('close', (hadError) => { + // indicate client connection end + self.server.emit( + EVENTS.PROXY_DISCONNECT, + originInfo, + destinationInfo, + hadError, + ); + }); - self.server.emit(EVENTS.PROXY_ERROR, err); + connectionFilterDomain.exit(); + }); - if (err.code && err.code === 'EADDRNOTAVAIL') { - return end(RFC_1928_REPLIES.HOST_UNREACHABLE, args); - } + // capture connection errors and response appropriately + destination.on('error', (err) => { + // exit the connection filter domain + connectionFilterDomain.exit(); - if (err.code && err.code === 'ECONNREFUSED') { - return end(RFC_1928_REPLIES.CONNECTION_REFUSED, args); - } + // notify of connection error + err.addr = args.dst.addr; + err.atyp = args.atyp; + err.port = args.dst.port; - return end(RFC_1928_REPLIES.NETWORK_UNREACHABLE, args); - }); + self.server.emit(EVENTS.PROXY_ERROR, err); - 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 + if (err.code && err.code === 'EADDRNOTAVAIL') { + return end(RFC_1928_REPLIES.HOST_UNREACHABLE, args); } - }; - destination.setTimeout(self.connectTimeout); - destination.once('timeout', onConnectTimeout); - destination.once('connect', () => { - destination.off('timeout', onConnectTimeout); - if (!self.destinationIdleTimeout) { - destination.setTimeout(0); + if (err.code && err.code === 'ECONNREFUSED') { + return end(RFC_1928_REPLIES.CONNECTION_REFUSED, args); } + + return end(RFC_1928_REPLIES.NETWORK_UNREACHABLE, args); }); + + 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); + } + }); + } } }), );