SSH2 client and server modules written in pure JavaScript for node.js
—
Comprehensive port forwarding capabilities for creating secure tunnels through SSH connections, including local forwarding, remote forwarding, and Unix domain socket support.
Forward connections from client through server to destination (client initiates connections).
/**
* Create local port forwarding connection (client -> server -> destination)
* @param srcIP - Source IP address (usually from client perspective)
* @param srcPort - Source port number
* @param dstIP - Destination IP address (from server perspective)
* @param dstPort - Destination port number
* @param callback - Callback receiving connection stream
* @returns false if connection not ready, true otherwise
*/
forwardOut(srcIP: string, srcPort: number, dstIP: string, dstPort: number, callback: ChannelCallback): boolean;
type ChannelCallback = (err: Error | null, stream?: ClientChannel) => void;Local Forwarding Examples:
const { Client } = require('ssh2');
const conn = new Client();
conn.on('ready', () => {
console.log('Client :: ready');
// Forward connection to database through SSH server
conn.forwardOut('127.0.0.1', 0, 'database.internal', 5432, (err, stream) => {
if (err) throw err;
console.log('Connected to database through SSH tunnel');
// Use stream as regular TCP connection
stream.on('data', (data) => {
console.log('Received:', data.toString());
});
stream.write('SELECT version();\n');
});
}).connect({
host: 'jump-server.com',
username: 'user',
privateKey: privateKey
});Set up server to listen on port and forward connections to client (server accepts connections).
/**
* Request remote port forwarding (server binds port and forwards to client)
* Server will listen on bindAddr:bindPort and forward connections to client
* @param bindAddr - Address for server to bind ('' for all interfaces)
* @param bindPort - Port for server to bind (0 for dynamic allocation)
* @param callback - Callback with actual bound port
* @returns false if connection not ready, true otherwise
*/
forwardIn(bindAddr: string, bindPort: number, callback: ForwardCallback): boolean;
/**
* Cancel remote port forwarding
* @param bindAddr - Address that was bound
* @param bindPort - Port that was bound
* @param callback - Callback with cancellation result
* @returns false if connection not ready, true otherwise
*/
unforwardIn(bindAddr: string, bindPort: number, callback: UnforwardCallback): boolean;
type ForwardCallback = (err: Error | null, bindPort?: number) => void;
type UnforwardCallback = (err: Error | null) => void;Remote Forwarding Examples:
const { Client } = require('ssh2');
const conn = new Client();
conn.on('ready', () => {
console.log('Client :: ready');
// Request server to listen on port 8080 and forward to client
conn.forwardIn('', 8080, (err, bindPort) => {
if (err) throw err;
console.log(`Server listening on port ${bindPort}`);
console.log('Remote connections will be forwarded to client');
});
}).on('tcp connection', (details, accept, reject) => {
console.log('Incoming connection:', details);
// Accept the forwarded connection
const stream = accept();
// Handle the connection (e.g., proxy to local service)
const net = require('net');
const localConnection = net.createConnection(3000, 'localhost');
stream.pipe(localConnection);
localConnection.pipe(stream);
stream.on('close', () => {
console.log('Forwarded connection closed');
localConnection.end();
});
}).connect({
host: 'public-server.com',
username: 'user',
privateKey: privateKey
});Forward Unix domain sockets for local IPC communication.
/**
* Request Unix domain socket forwarding (OpenSSH extension)
* Server will listen on Unix socket and forward connections to client
* @param socketPath - Unix socket path on server
* @param callback - Callback with operation result
* @returns false if connection not ready, true otherwise
*/
openssh_forwardInStreamLocal(socketPath: string, callback: ForwardCallback): boolean;
/**
* Cancel Unix domain socket forwarding (OpenSSH extension)
* @param socketPath - Unix socket path to unbind
* @param callback - Callback with cancellation result
* @returns false if connection not ready, true otherwise
*/
openssh_unforwardInStreamLocal(socketPath: string, callback: UnforwardCallback): boolean;
/**
* Connect to Unix domain socket through server (OpenSSH extension)
* @param socketPath - Unix socket path on server to connect to
* @param callback - Callback receiving connection stream
* @returns false if connection not ready, true otherwise
*/
openssh_forwardOutStreamLocal(socketPath: string, callback: ChannelCallback): boolean;Unix Socket Forwarding Examples:
const { Client } = require('ssh2');
const conn = new Client();
conn.on('ready', () => {
console.log('Client :: ready');
// Forward Unix socket from server to client
conn.openssh_forwardInStreamLocal('/tmp/remote-socket', (err) => {
if (err) throw err;
console.log('Unix socket forwarding established');
});
// Connect to Unix socket on server
conn.openssh_forwardOutStreamLocal('/var/run/docker.sock', (err, stream) => {
if (err) throw err;
console.log('Connected to Docker socket through SSH');
// Use stream to communicate with Docker daemon
stream.write('GET /containers/json HTTP/1.1\r\nHost: localhost\r\n\r\n');
});
}).on('unix connection', (info, accept, reject) => {
console.log('Incoming Unix connection:', info.socketPath);
const stream = accept();
// Handle Unix socket connection...
}).connect({
host: 'docker-host.com',
username: 'user',
privateKey: privateKey
});Handle port forwarding requests when operating as SSH server.
/**
* Handle direct TCP/IP connection requests (local forwarding from client)
*/
client.on('tcpip', (accept, reject, info: TcpipInfo) => {
console.log(`Client wants to connect to ${info.destIP}:${info.destPort}`);
if (allowConnection(info)) {
const stream = accept();
// Set up connection to destination
const net = require('net');
const connection = net.createConnection(info.destPort, info.destIP);
stream.pipe(connection);
connection.pipe(stream);
} else {
reject();
}
});
/**
* Handle OpenSSH streamlocal connection requests (Unix socket forwarding)
*/
client.on('openssh.streamlocal', (accept, reject, info: StreamLocalInfo) => {
console.log(`Client wants to connect to Unix socket: ${info.socketPath}`);
const stream = accept();
const net = require('net');
const connection = net.createConnection(info.socketPath);
stream.pipe(connection);
connection.pipe(stream);
});
interface TcpipInfo {
srcIP: string;
srcPort: number;
destIP: string;
destPort: number;
}
interface StreamLocalInfo {
socketPath: string;
}/**
* Handle global requests for port forwarding setup
*/
client.on('request', (accept, reject, name: string, info: RequestInfo) => {
console.log(`Global request: ${name}`, info);
switch (name) {
case 'tcpip-forward':
// Set up remote port forwarding
console.log(`Binding ${info.bindAddr}:${info.bindPort}`);
const server = net.createServer((connection) => {
// Forward incoming connections to client
client.forwardOut(
info.bindAddr, info.bindPort,
connection.remoteAddress, connection.remotePort,
(err, stream) => {
if (!err) {
connection.pipe(stream);
stream.pipe(connection);
} else {
connection.end();
}
}
);
});
server.listen(info.bindPort, info.bindAddr, () => {
accept(); // Accept the forwarding request
});
break;
case 'cancel-tcpip-forward':
// Cancel remote port forwarding
console.log(`Unbinding ${info.bindAddr}:${info.bindPort}`);
// Clean up server listening on this port
accept();
break;
case 'streamlocal-forward@openssh.com':
// Set up Unix socket forwarding
console.log(`Binding Unix socket: ${info.socketPath}`);
accept();
break;
default:
reject();
}
});
interface RequestInfo {
bindAddr?: string;
bindPort?: number;
socketPath?: string;
[key: string]: any;
}Create SOCKS proxy functionality using port forwarding.
const { Client } = require('ssh2');
const net = require('net');
function createSOCKSProxy(sshConfig, localPort) {
const sshClient = new Client();
// Create local SOCKS server
const socksServer = net.createServer((clientSocket) => {
let stage = 0;
let targetHost, targetPort;
clientSocket.on('data', (data) => {
if (stage === 0) {
// SOCKS handshake
if (data[0] === 0x05) {
clientSocket.write(Buffer.from([0x05, 0x00])); // No auth required
stage = 1;
}
} else if (stage === 1) {
// SOCKS connect request
if (data[0] === 0x05 && data[1] === 0x01) {
const addrType = data[3];
let offset = 4;
if (addrType === 0x01) {
// IPv4
targetHost = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
offset = 8;
} else if (addrType === 0x03) {
// Domain name
const domainLen = data[4];
targetHost = data.slice(5, 5 + domainLen).toString();
offset = 5 + domainLen;
}
targetPort = data.readUInt16BE(offset);
// Forward through SSH
sshClient.forwardOut('127.0.0.1', 0, targetHost, targetPort, (err, stream) => {
if (err) {
clientSocket.write(Buffer.from([0x05, 0x01])); // General failure
clientSocket.end();
} else {
clientSocket.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); // Success
clientSocket.pipe(stream);
stream.pipe(clientSocket);
}
});
}
}
});
});
sshClient.on('ready', () => {
socksServer.listen(localPort, () => {
console.log(`SOCKS proxy listening on port ${localPort}`);
});
}).connect(sshConfig);
return { sshClient, socksServer };
}
// Usage
const proxy = createSOCKSProxy({
host: 'ssh-server.com',
username: 'user',
privateKey: privateKey
}, 1080);Chain multiple SSH connections for complex routing.
const { Client } = require('ssh2');
function createMultiHopTunnel() {
const jump1 = new Client();
const jump2 = new Client();
jump1.on('ready', () => {
console.log('Jump1 connected');
// Connect through first jump server to second
jump1.forwardOut('127.0.0.1', 0, 'internal-jump.corp', 22, (err, stream) => {
if (err) throw err;
// Use first tunnel as transport for second SSH connection
jump2.connect({
username: 'user2',
privateKey: privateKey2,
sock: stream
});
});
}).connect({
host: 'external-jump.com',
username: 'user1',
privateKey: privateKey1
});
jump2.on('ready', () => {
console.log('Jump2 connected through Jump1');
// Now forward to final destination through both jumps
jump2.forwardOut('127.0.0.1', 0, 'secure-server.corp', 3389, (err, stream) => {
if (err) throw err;
console.log('Connected to final destination through multi-hop tunnel');
// Use stream for RDP connection or other protocol
});
});
}interface TcpConnectionDetails {
srcIP: string; // Source IP address
srcPort: number; // Source port number
destIP: string; // Destination IP address
destPort: number; // Destination port number
}
// Client event for incoming forwarded connections
client.on('tcp connection', (details: TcpConnectionDetails, accept: AcceptConnection, reject: RejectConnection) => {
console.log(`Incoming TCP connection from ${details.srcIP}:${details.srcPort} to ${details.destIP}:${details.destPort}`);
const stream = accept();
// Handle the forwarded connection...
});interface UnixConnectionInfo {
socketPath: string; // Unix socket path
}
// Client event for incoming Unix socket connections
client.on('unix connection', (info: UnixConnectionInfo, accept: AcceptConnection, reject: RejectConnection) => {
console.log(`Incoming Unix connection to ${info.socketPath}`);
const stream = accept();
// Handle the Unix socket connection...
});type AcceptConnection<T = ClientChannel> = () => T;
type RejectConnection = () => boolean;
interface ClientChannel extends Duplex {
stderr: Duplex;
setWindow(rows: number, cols: number, height?: number, width?: number): boolean;
signal(signalName: string): boolean;
exit(status: number): boolean;
}// Restrict forwarding destinations
client.on('tcpip', (accept, reject, info) => {
const allowedHosts = ['127.0.0.1', 'localhost', '192.168.1.0/24'];
const allowedPorts = [80, 443, 3000, 8080];
if (isAllowedDestination(info.destIP, allowedHosts) &&
allowedPorts.includes(info.destPort)) {
accept();
} else {
console.log(`Rejected connection to ${info.destIP}:${info.destPort}`);
reject();
}
});
// Rate limiting
const connectionCounts = new Map();
client.on('tcpip', (accept, reject, info) => {
const key = `${info.srcIP}:${info.srcPort}`;
const count = connectionCounts.get(key) || 0;
if (count > 10) {
console.log(`Rate limit exceeded for ${key}`);
reject();
} else {
connectionCounts.set(key, count + 1);
const stream = accept();
stream.on('close', () => {
connectionCounts.set(key, Math.max(0, connectionCounts.get(key) - 1));
});
}
});// Track active forwards
const activeForwards = new Map();
function createForward(remoteAddr, remotePort, localAddr, localPort) {
const key = `${remoteAddr}:${remotePort}`;
if (activeForwards.has(key)) {
console.log(`Forward already exists: ${key}`);
return;
}
conn.forwardIn(remoteAddr, remotePort, (err, bindPort) => {
if (err) {
console.error(`Failed to create forward: ${err.message}`);
} else {
activeForwards.set(key, { bindPort, localAddr, localPort });
console.log(`Forward created: ${remoteAddr}:${bindPort} -> ${localAddr}:${localPort}`);
}
});
}
function removeForward(remoteAddr, remotePort) {
const key = `${remoteAddr}:${remotePort}`;
const forward = activeForwards.get(key);
if (forward) {
conn.unforwardIn(remoteAddr, forward.bindPort, (err) => {
if (!err) {
activeForwards.delete(key);
console.log(`Forward removed: ${key}`);
}
});
}
}Install with Tessl CLI
npx tessl i tessl/npm-ssh2