Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Server, Socket } from 'net';
import * as stream from 'stream';

export interface DestinationInfo {
address: string;
Expand All @@ -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,
Expand Down
266 changes: 149 additions & 117 deletions src/socks5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
}
});
}
}
}),
);
Expand Down