CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ssh2

SSH2 client and server modules written in pure JavaScript for node.js

Pending
Overview
Eval results
Files

port-forwarding.mddocs/

Port Forwarding

Comprehensive port forwarding capabilities for creating secure tunnels through SSH connections, including local forwarding, remote forwarding, and Unix domain socket support.

Capabilities

Local Port Forwarding

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
});

Remote Port Forwarding

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
});

Unix Domain Socket Forwarding (OpenSSH Extensions)

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
});

Server-Side Port Forwarding

Handle port forwarding requests when operating as SSH server.

Connection Request Events

/**
 * 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;
}

Global Request Handling

/**
 * 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;
}

Advanced Port Forwarding Patterns

Dynamic Port Forwarding (SOCKS Proxy)

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);

Multi-Hop Port Forwarding

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
    });
  });
}

Connection Event Details

TCP Connection Events

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...
});

Unix Connection Events

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 Definitions

Connection Handling Types

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;
}

Best Practices

Security Considerations

// 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));
    });
  }
});

Connection Management

// 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

docs

http-tunneling.md

index.md

key-management.md

port-forwarding.md

sftp-operations.md

ssh-agents.md

ssh-client.md

ssh-server.md

tile.json