SSH Tunneling and Port Forwarding
Implementation Reference
Source Files:
packages/core/src/ssh/port-forwarding.ts
- Port forwarding implementationpackages/core/src/ssh/tunnel.ts
- Tunnel managementpackages/core/src/ssh/jump-host.ts
- Jump host connectionspackages/core/src/ssh/socks.ts
- SOCKS proxy implementation
Key Functions:
createTunnel()
- Create SSH tunnel (lines 15-85)forwardPort()
- Port forwarding setup (lines 90-145)createSOCKSProxy()
- SOCKS proxy creation (lines 20-75)connectViaJumpHost()
- Jump host connection (lines 30-95)
Local Port Forwarding
Basic Local Forwarding
Forward a local port to a remote service:
// Forward local:8080 → remote:3000
const tunnel = await $.ssh('user@host').forward({
localPort: 8080,
remoteHost: 'localhost',
remotePort: 3000
});
// Access remote service locally
const response = await fetch('http://localhost:8080');
// Close tunnel when done
await tunnel.close();
Configuration
targets:
database-tunnel:
type: ssh
host: db.example.com
user: admin
tunnels:
- type: local
localPort: 5432
remoteHost: localhost
remotePort: 5432
description: PostgreSQL tunnel
Advanced Local Forwarding
// Forward to different remote host
const tunnel = await $.ssh('user@bastion').forward({
localHost: '127.0.0.1', // Bind address (default: 127.0.0.1)
localPort: 3306, // Local port
remoteHost: 'db.internal', // Remote destination
remotePort: 3306 // Remote port
});
// Multiple tunnels
const tunnels = await Promise.all([
$.ssh('user@host').forward({ localPort: 8080, remotePort: 80 }),
$.ssh('user@host').forward({ localPort: 8443, remotePort: 443 }),
$.ssh('user@host').forward({ localPort: 3306, remotePort: 3306 })
]);
// Dynamic local port
const tunnel = await $.ssh('user@host').forward({
localPort: 0, // Use any available port
remotePort: 3000
});
console.log(`Tunnel on port: ${tunnel.localPort}`);
Use Cases
// 1. Database access
const dbTunnel = await $.ssh('user@dbserver').forward({
localPort: 5432,
remoteHost: 'localhost',
remotePort: 5432
});
// Connect to database via tunnel
const db = new Client({
host: 'localhost',
port: 5432,
database: 'production'
});
// 2. Web service access
const webTunnel = await $.ssh('user@webserver').forward({
localPort: 8080,
remoteHost: 'internal-api',
remotePort: 80
});
// Access internal API
const api = axios.create({
baseURL: 'http://localhost:8080'
});
// 3. Redis access
const redisTunnel = await $.ssh('user@redis').forward({
localPort: 6379,
remotePort: 6379
});
const redis = new Redis({
host: 'localhost',
port: 6379
});
Remote Port Forwarding
Basic Remote Forwarding
Expose local service to remote server:
// Make local:3000 available as remote:8080
const tunnel = await $.ssh('user@host').reverseForward({
remotePort: 8080,
localHost: 'localhost',
localPort: 3000
});
// Remote server can now access your local service
// http://localhost:8080 on remote → http://localhost:3000 locally
Configuration
targets:
expose-local:
type: ssh
host: public.example.com
tunnels:
- type: remote
remotePort: 8080
localHost: localhost
localPort: 3000
description: Expose local dev server
Advanced Remote Forwarding
// Bind to specific interface on remote
const tunnel = await $.ssh('user@host').reverseForward({
remoteHost: '0.0.0.0', // Bind to all interfaces
remotePort: 8080,
localHost: 'localhost',
localPort: 3000
});
// Multiple services
const tunnels = [
{ remotePort: 8080, localPort: 3000 }, // Web app
{ remotePort: 8081, localPort: 3001 }, // API
{ remotePort: 8082, localPort: 3002 } // Admin panel
];
for (const config of tunnels) {
await $.ssh('user@host').reverseForward(config);
}
Use Cases
// 1. Share local development server
const devTunnel = await $.ssh('user@staging').reverseForward({
remotePort: 8080,
localPort: 3000
});
console.log('Dev server available at: http://staging.example.com:8080');
// 2. Webhook testing
const webhookTunnel = await $.ssh('user@public').reverseForward({
remoteHost: '0.0.0.0',
remotePort: 9000,
localPort: 4000
});
// Register http://public.example.com:9000 as webhook URL
// 3. Remote debugging
const debugTunnel = await $.ssh('user@remote').reverseForward({
remotePort: 9229,
localPort: 9229
});
// Attach debugger to remote:9229
Dynamic Port Forwarding (SOCKS)
SOCKS Proxy Setup
// Create SOCKS proxy on local:1080
const proxy = await $.ssh('user@host').socks({
port: 1080, // Local SOCKS port
host: '127.0.0.1' // Bind address
});
// Use proxy for HTTP requests
const agent = new SocksProxyAgent('socks5://localhost:1080');
const response = await fetch('http://internal-service', {
agent
});
// Close proxy
await proxy.close();
Configuration
targets:
socks-proxy:
type: ssh
host: proxy.example.com
tunnels:
- type: socks
port: 1080
description: SOCKS5 proxy
Using SOCKS Proxy
// 1. With HTTP client
import { SocksProxyAgent } from 'socks-proxy-agent';
const proxy = await $.ssh('user@host').socks({ port: 1080 });
const agent = new SocksProxyAgent('socks5://localhost:1080');
const response = await axios.get('http://internal-api', {
httpsAgent: agent,
httpAgent: agent
});
// 2. With curl via command line
await $.ssh('user@host').socks({ port: 1080 });
await $`curl --socks5 localhost:1080 http://internal-service`;
// 3. System-wide proxy (macOS)
await $`networksetup -setsocksfirewallproxy Wi-Fi localhost 1080`;
// ... use proxy ...
await $`networksetup -setsocksfirewallproxystate Wi-Fi off`;
SOCKS with Authentication
// SOCKS proxy with authentication
const proxy = await $.ssh('user@host').socks({
port: 1080,
auth: {
username: 'proxy-user',
password: 'proxy-pass'
}
});
// Use with auth
const agent = new SocksProxyAgent('socks5://proxy-user:proxy-pass@localhost:1080');
Jump Hosts (Bastion)
Single Jump Host
targets:
private-server:
type: ssh
host: 10.0.1.50 # Private IP
user: admin
jumpHost:
host: bastion.example.com
user: jump-user
privateKey: ~/.ssh/bastion_key
// Programmatic jump host
await $.ssh({
host: '10.0.1.50',
user: 'admin',
privateKey: '~/.ssh/private_key',
jumpHost: {
host: 'bastion.example.com',
user: 'jump-user',
privateKey: '~/.ssh/bastion_key'
}
})`command`;
Multiple Jump Hosts
// Chain of jump hosts
await $.ssh({
host: 'final-destination',
user: 'admin',
jumpHosts: [
{
host: 'jump1.example.com',
user: 'user1',
privateKey: '~/.ssh/key1'
},
{
host: 'jump2.internal',
user: 'user2',
privateKey: '~/.ssh/key2'
}
]
})`command`;
Jump Host with Tunnels
// Tunnel through jump host
const tunnel = await $.ssh({
host: 'bastion.example.com',
user: 'jump'
}).forward({
localPort: 5432,
remoteHost: 'db.internal', // Accessible from bastion
remotePort: 5432
});
// Connect to internal database
const db = new Client({
host: 'localhost',
port: 5432
});
Complex Tunneling Scenarios
Multi-Hop Tunneling
// Tunnel through multiple hosts
async function createMultiHopTunnel(hops: Array<SSHConfig>, finalPort: number) {
let localPort = 10000;
const tunnels = [];
for (let i = 0; i < hops.length - 1; i++) {
const tunnel = await $.ssh(hops[i]).forward({
localPort: localPort + i,
remoteHost: hops[i + 1].host,
remotePort: i === hops.length - 2 ? finalPort : 22
});
tunnels.push(tunnel);
}
return {
port: localPort,
close: async () => {
for (const tunnel of tunnels.reverse()) {
await tunnel.close();
}
}
};
}
// Usage
const tunnel = await createMultiHopTunnel([
{ host: 'jump1.example.com', user: 'user1' },
{ host: 'jump2.internal', user: 'user2' },
{ host: 'target.private', user: 'admin' }
], 3306); // MySQL on final host
Bidirectional Tunneling
// Simultaneous local and remote forwarding
const ssh = await $.ssh('user@host');
// Local forward
const localTunnel = await ssh.forward({
localPort: 8080,
remotePort: 80
});
// Remote forward
const remoteTunnel = await ssh.reverseForward({
remotePort: 9000,
localPort: 3000
});
// Both tunnels active simultaneously
Dynamic Tunnel Management
class TunnelManager {
private tunnels = new Map<string, SSHTunnel>();
async create(name: string, config: TunnelConfig): Promise<void> {
if (this.tunnels.has(name)) {
throw new Error(`Tunnel ${name} already exists`);
}
const tunnel = await $.ssh(config.ssh).forward(config.forward);
this.tunnels.set(name, tunnel);
// Auto-reconnect on failure
tunnel.on('error', async () => {
console.log(`Tunnel ${name} failed, reconnecting...`);
await this.recreate(name, config);
});
}
async recreate(name: string, config: TunnelConfig): Promise<void> {
await this.close(name);
await this.create(name, config);
}
async close(name: string): Promise<void> {
const tunnel = this.tunnels.get(name);
if (tunnel) {
await tunnel.close();
this.tunnels.delete(name);
}
}
async closeAll(): Promise<void> {
for (const [name, tunnel] of this.tunnels) {
await tunnel.close();
}
this.tunnels.clear();
}
}
Performance Optimization
Connection Reuse
// Reuse SSH connection for multiple tunnels
const ssh = await SSHClient.connect({
host: 'tunnel.example.com',
user: 'admin'
});
// Create multiple tunnels on same connection
const tunnels = await Promise.all([
ssh.forwardOut({ srcPort: 8080, dstPort: 80 }),
ssh.forwardOut({ srcPort: 8443, dstPort: 443 }),
ssh.forwardOut({ srcPort: 3306, dstPort: 3306 })
]);
// All tunnels share one SSH connection
Compression
// Enable compression for tunnel traffic
const tunnel = await $.ssh({
host: 'host.example.com',
compress: true // Compress tunnel data
}).forward({
localPort: 8080,
remotePort: 80
});
Keep-Alive
// Prevent tunnel timeout
const tunnel = await $.ssh({
host: 'host.example.com',
keepaliveInterval: 10000, // 10 seconds
keepaliveCountMax: 3
}).forward({
localPort: 8080,
remotePort: 80
});
Security Considerations
Tunnel Security
// Secure tunnel configuration
const secureTunnel = await $.ssh({
host: 'secure.example.com',
// Strong authentication
privateKey: process.env.SSH_KEY,
// Host verification
strictHostKeyChecking: true,
// Strong algorithms
algorithms: {
cipher: ['aes256-gcm@openssh.com'],
kex: ['curve25519-sha256']
}
}).forward({
// Bind only to localhost
localHost: '127.0.0.1',
localPort: 8080,
remotePort: 80
});
Access Control
// Limit tunnel access
const tunnel = await $.ssh('user@host').forward({
localHost: '127.0.0.1', // Only localhost can connect
localPort: 8080,
remotePort: 80,
// Custom access control
onConnection: (info) => {
// Validate connection
if (!isAllowedClient(info.srcAddr)) {
throw new Error('Connection refused');
}
}
});
Monitoring and Debugging
Tunnel Status
// Monitor tunnel status
class TunnelMonitor {
async checkTunnel(tunnel: SSHTunnel): Promise<boolean> {
try {
// Test tunnel with simple request
const response = await fetch(`http://localhost:${tunnel.localPort}/health`);
return response.ok;
} catch {
return false;
}
}
async monitorTunnels(tunnels: SSHTunnel[]) {
setInterval(async () => {
for (const tunnel of tunnels) {
const healthy = await this.checkTunnel(tunnel);
console.log(`Tunnel ${tunnel.localPort}: ${healthy ? 'UP' : 'DOWN'}`);
if (!healthy) {
// Alert or restart tunnel
await this.restartTunnel(tunnel);
}
}
}, 30000); // Check every 30 seconds
}
}
Debug Logging
// Enable tunnel debugging
process.env.DEBUG = 'ssh2:*,xec:tunnel:*';
const tunnel = await $.ssh('user@host').forward({
localPort: 8080,
remotePort: 80,
debug: (msg) => console.log(`TUNNEL: ${msg}`)
});
Common Issues and Solutions
Port Already in Use
// Find available port
async function findAvailablePort(start = 10000): Promise<number> {
const net = require('net');
return new Promise((resolve) => {
const server = net.createServer();
server.listen(start, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on('error', () => {
resolve(findAvailablePort(start + 1));
});
});
}
const port = await findAvailablePort();
const tunnel = await $.ssh('user@host').forward({
localPort: port,
remotePort: 80
});
Tunnel Timeout
// Handle tunnel timeouts
const tunnel = await $.ssh({
host: 'host.example.com',
// Increase timeouts
connectionTimeout: 60000,
// Keep connection alive
keepaliveInterval: 10000
}).forward({
localPort: 8080,
remotePort: 80
});
// Restart on timeout
tunnel.on('timeout', async () => {
console.log('Tunnel timeout, restarting...');
await tunnel.restart();
});
Best Practices
- Close tunnels when done to free resources
- Use connection pooling for multiple tunnels
- Monitor tunnel health in production
- Implement auto-reconnect for critical tunnels
- Use compression for slow networks
- Secure tunnel endpoints with proper binding
- Log tunnel activity for debugging
Related Documentation
- SSH Overview - SSH fundamentals
- Connection Config - Connection setup
- Batch Operations - Multi-host operations
- SSH Authentication - Authentication methods