GitHub - brozeph/simple-socks: Simple SOCKS5 proxy server (original) (raw)

Creates a simple SOCKS5 server and exposes additional SOCKS5 proxy events.

CI codecov npm version license node

Installation

Common Contributor Workflow

For local development in this repository:

npm ci npm run format npm run lint npm run test npm run test:coverage

Quick checks before opening a PR:

Example Usage

In the examples folder exists two examples, one that requires no authentication and one that requires username/password authentication. Below is an example with no authentication:

import socks5 from "simple-socks";

const server = socks5.createServer().listen(1080);

// When a reqest arrives for a remote destination server.on("proxyConnect", (info, destination) => { console.log("connected to remote server at %s:%d", info.address, info.port);

destination.on("data", (data) => { console.log(data.length); }); });

// When data arrives from the remote connection server.on("proxyData", (data) => { console.log(data.length); });

// When an error occurs connecting to remote destination server.on("proxyError", (err) => { console.error("unable to connect to remote server"); console.error(err); });

// When a request for a remote destination ends server.on("proxyDisconnect", (originInfo, destinationInfo, hadError) => { console.log( "client %s:%d request has disconnected from remote server at %s:%d with %serror", originInfo.address, originInfo.port, destinationInfo.address, destinationInfo.port, hadError ? "" : "no ", ); });

// When a proxy connection ends server.on("proxyEnd", (response, args) => { console.log("socket closed with code %d", response); console.log(args); });

Running The Examples

No Authentication

For a SOCKS5 server that does not require authentication, look at examples/createServer.js:

node examples/createServer

In a separate terminal window:

curl http://www.google.com --socks5 127.0.0.1:1080

Username/Password Authentication

For a SOCKS5 server that requires username/password authentication, look at examples/createServerWithAuthentication.js:

node examples/createServerWithAuthentication

In a separate terminal window:

curl http://www.google.com --socks5 127.0.0.1:1080 --proxy-user foo:bar

Connection Filter

For a SOCKS5 server that can perform either origin or destination (or both!) address filtering, look at examples/createServerConnectionFilter.js:

node examples/createServerConnectionFilter

In a separate terminal window:

curl http://www.github.com --socks5 127.0.0.1:1080 # allowed curl http://www.google.com --socks5 127.0.0.1:1080 # denied

Multiple Servers

For running multiple SOCKS5 servers on the same port, but bound to different interfaces, look at examples/createMultipleServers.js:

node examples/createMultipleServers

GSSAPI/Negotiate (Kerberos)

To run a SOCKS5 server that authenticates via GSSAPI/Negotiate using a minimal provider backed by the kerberos module:

Prerequisites:

Run the example:

node examples/createServerWithGssapi

Client test (example with curl):

curl --socks5 127.0.0.1:1080
--socks5-gssapi
--socks5-gssapi-service rcmd
https://example.com

Provider reference: examples/gssapiKerberosProvider.js.

Methods

createServer

Factory method that creates an instance of a SOCKS5 proxy server:

import socks5 from "simple-socks";

const server = socks5.createServer();

server.listen(1080, "0.0.0.0", function () { console.log("SOCKS5 proxy server started on 0.0.0.0:1080"); });

This method accepts an optional options argument:

authentication callback

To make the socks5 server require username/password authentication, supply a function callback in the options as follows:

import socks5 from "simple-socks";

const options = { authenticate: function (username, password, socket, callback) { if (username === "foo" && password === "bar") { return setImmediate(callback); }

return setImmediate(callback, new Error("incorrect username and password"));

}, };

const server = socks5.createServer(options);

// begin listening and require user/pass authentication server.listen(1080);

The authenticate callback accepts three arguments:

compatAuth options

compatAuth is optional and disabled by default. It only affects RFC 1929 username/password payload validation.

const server = socks5.createServer({ authenticate(username, password, socket, callback) { if (username === "foo" && password === "") { return setImmediate(callback); }

return setImmediate(callback, new Error("bad credentials"));

}, compatAuth: { allowEmptyUsername: false, // default false allowEmptyPassword: true, // default false strictMethodNegotiation: true, // must remain true }, });

Behavior:

compatAuth is not a server equivalent of client flags such as curl --proxy-user; proxy credentials are still sent (or not sent) by the client.

connectionFilter callback

Allows you to filter incoming connections, based on either origin and/or destination, return false to disallow:

import socks5 from "simple-socks";

const server = socks5.createServer({ connectionFilter: function (destination, origin, callback) { if (origin.address === "127.0.0.1") { console.log("denying access from %s:%s", origin.address, origin.port);

  return setImmediate(
    callback,
    new Error("access from specified origin is denied"),
  );
}

if (destination.address === "10.0.0.1") {
  console.log("denying access to %s:%s", remote.address, remote.port);

  return setImmediate(
    callback,
    new Error("access to specified destination is denied"),
  );
}

return setImmediate(callback);

}, });

The connectionFilter callback accepts three arguments:

For an example, see examples/createServerConnectionFilter.js.

connectionOptions callback

Allows you to customize the options passed to net.createConnection after a request passes connectionFilter. This callback can rewrite the outbound destination, choose a local bind address, or route traffic through a local TCP adapter. It must return TCP connection options; it does not replace the destination with an arbitrary stream.

import socks5 from "simple-socks";

const server = socks5.createServer({ connectionOptions(destination, origin, defaults, callback) { return callback(null, { ...defaults, host: "127.0.0.1", port: destination.port === 443 ? 8443 : destination.port, }); }, });

The connectionOptions callback accepts four arguments:

To route requests through an SSH tunnel while keeping simple-socks responsible for creating a real net.Socket, bridge each request through a temporary local TCP listener. The listener accepts the socket created by simple-socks, then pipes it to the SSH channel returned by forwardOut:

import net from "net"; import socks5 from "simple-socks";

function createSshForwardTarget(ssh, destination, origin, callback) { const forwarder = net.createServer((localSocket) => { forwarder.close();

ssh.forwardOut(
  origin.address,
  origin.port,
  destination.address,
  destination.port,
  (err, sshStream) => {
    if (err) {
      localSocket.destroy(err);
      return;
    }

    localSocket.pipe(sshStream);
    sshStream.pipe(localSocket);

    localSocket.once("close", () => sshStream.destroy());
    localSocket.once("error", () => sshStream.destroy());
    sshStream.once("close", () => localSocket.destroy());
    sshStream.once("error", () => localSocket.destroy());
  },
);

});

forwarder.once("error", callback); forwarder.listen(0, "127.0.0.1", () => { const address = forwarder.address();

callback(null, {
  host: address.address,
  port: address.port,
});

}); }

const server = socks5.createServer({ connectionOptions(destination, origin, defaults, callback) { createSshForwardTarget(ssh, destination, origin, (err, target) => { if (err) { return callback(err); }

  return callback(null, {
    ...defaults,
    ...target,
  });
});

}, });

This pattern keeps the SOCKS server's outbound side TCP-based. If you already have a stream.Duplex from an SSH client or another transport, adapt it behind a local TCP listener instead of returning the stream directly.

Multiple Interfaces

If your machine has multiple IP addresses and you want a separate SOCKS5 server bound to each one, create a server per interface and bind explicitly using the object form of listen with exclusive: true:

import socks5 from "simple-socks";

const hosts = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]; const port = 1080;

hosts.forEach((host) => { const server = socks5.createServer();

server.on("listening", () => { const addr = server.address(); // should show the specific host, not 0.0.0.0 console.log("listening on %s:%d", addr.address, addr.port); });

// Force an interface-specific, exclusive bind server.listen({ port, host, exclusive: true }); });

Notes:

See also: examples/createMultipleServers.js.

GSSAPI/Negotiate (Phase 1)

This library can be configured to prefer GSSAPI/Negotiate (RFC 1961) when a provider is supplied. This callback allows code to delegate the GSS exchange to a pluggable provider and, upon success, proceeds without wrapping subsequent data (auth-only).

Enable GSSAPI by supplying a gssapi option with a provider implementing authenticate(socket, firstChunk, callback):

import socks5 from "simple-socks";

const server = socks5.createServer({ gssapi: { enabled: true, provider: { authenticate(socket, firstChunk, callback) { // Implement RFC 1961 token exchange here. // Use system GSS libraries via your integration and call callback(err, principal) // on success with the authenticated principal string. callback(new Error("GSSAPI provider not implemented")); }, }, }, });

server.listen(1080);

Notes:

Events

The socks5 server supports all events that exist on a native net.Server object. Additionally, the following events have been added that are specific to the SOCKS5 proxy:

Note:

This module exports the above events as constants for convenience purposes via the property events:

console.log(socks5.events);

Outputs the following:

{ AUTHENTICATION: 'authenticate', AUTHENTICATION_ERROR: 'authenticateError', CONNECTION_FILTER: 'connectionFilter', HANDSHAKE: 'handshake', PROXY_CONNECT: 'proxyConnect', PROXY_DATA: 'proxyData', PROXY_DISCONNECT: 'proxyDisconnect', PROXY_END: 'proxyEnd', PROXY_ERROR: 'proxyError' }

handshake

This is event is emitted when a socks5 client connects to the server. The callback accepts a single argument:

// When a new request is initiated server.on("handshake", function (socket) { console.log( "new socks5 client from %s:%d", socket.remoteAddress, socket.remotePort, ); });

authenticate

This event is emitted when successful authentication occurs. The callback accepts a single argument:

// When authentication succeeds server.on("authenticate", function (username) { console.log("user %s successfully authenticated!", username); });

authenticateError

This event is emitted when authentication is not successful. The callback accepts the following arguments:

// When authentication fails server.on("authenticateError", function (username, err) { console.log("user %s failed to authenticate...", username); console.error(err); });

connectionFilter

This event is emitted when a destination address and port is filtered by the connectionFilter callback. The callback accepts the following arguments:

// When a destination connection is filtered server.on("connectionFilter", function (port, address, err) { console.log("connection to %s:%s has been denied", address, port); console.error(err); });

proxyConnect

This event is emitted each time a connection is requested to a remote destination. The callback accepts two arguments:

// When a reqest arrives for a remote destination server.on("proxyConnect", function (info, destination) { console.log("connected to remote server at %s:%d", info.address, info.port); });

proxyData

This event is emitted each time a remote connection returns data:

// When a reqest arrives for a remote destination server.on("proxyData", function (data) { console.log("data received from remote destination: %d", data.length); });

*_Note:_- This can also be accomplished by listening to the data event on the destination connection received in the proxyConnect event:

// When a reqest arrives for a remote destination server.on("proxyConnect", function (info, destination) { destination.on("data", function (data) { console.log("data received from remote destination: %d", data.length); }); });

proxyDisconnect

This event is emitted after a proxyConnect when a connection to a remote destination has ended. The callback accepts three arguments:

// When a request for a remote destination ends server.on("proxyDisconnect", function (err) { console.log( "client %s:%d request has disconnected from remote server at %s:%d with %serror", originInfo.address, originInfo.port, destinationInfo.address, destinationInfo.port, hadError ? "" : "no ", ); });

proxyError

In the event that a network error occurs attempting to create communication with the destination, this event is raised.

// When an error occurs connecting to remote destination server.on("proxyError", function (err) { console.error("unable to connect to remote server"); console.error(err); });

proxyEnd

When a socket connection is closed by the server, the proxyEnd event is emitted. It returns two arguments in the callback:

// When a proxy connection ends server.on("proxyEnd", function (response, args) { console.log("socket closed with code %d", response); console.log(args); });

macOS Authentication Notes

Some versions of the macOS built‑in SOCKS client (used when enabling a SOCKS proxy in Network settings) do not correctly implement RFC 1929 username/password authentication. They may not advertise BASIC (0x02) during method negotiation, or send a zero‑length password during the RFC 1929 sub‑negotiation.

If you require username/password auth from macOS clients, use a client that supports RFC 1929 (for example, curl --socks5 --proxy-user, or browsers/extensions that implement SOCKS5 BASIC). Alternatively, consider a different method such as GSSAPI/Negotiate on both client and server; the built‑in macOS client may favor that, but it is not implemented by this library.