Skip to content

Instantly share code, notes, and snippets.

@moeiscool
Last active April 3, 2025 07:00
Show Gist options
  • Save moeiscool/6204d56cb4ea09241a8b2028db3b64f1 to your computer and use it in GitHub Desktop.
Save moeiscool/6204d56cb4ea09241a8b2028db3b64f1 to your computer and use it in GitHub Desktop.
Simple Remote SSH over Websocket Protocol with Node.js

Simple Remote SSH over Websocket Protocol with Node.js (Alpha)

by Shinobi Systems

Based on the Simple TCP Proxy by kfox at https://gist.github.com/kfox/2313683

With this you spawn a websocket server on a server to allow SSH to a remote machine. When a client is connected it will be available on 9022 of the server. Every new client connected will be on port proceeding 9022. So if a server is already on 9022 the next one will be on 9023 and the next on 9024 and so on. Ports are released 24 hours after disconnection to be used again later.

Server machine run :

node server.js BIND_ADDRESS

Example :

node server.js 0.0.0.0:8090

Client machine run :

node client.js HOST IDENTIFIER

Example :

node client.js 127.0.0.1:8090 someserverid
const WebSocket = require('ws');
const net = require('net');
const process = require('process');
// Configuration
if (process.argv.length < 4) {
console.log('Usage: node client.js <host:port> <identifier>');
process.exit(1);
}
const WS_SERVER = process.argv[2];
const IDENTIFIER = process.argv[3];
const SSH_PORT = 22;
const RECONNECT_INTERVAL = 10000;
let ws = null;
let sshSocket = null;
let assignedPort = null;
let reconnectTimer = null;
function connectWebSocket() {
if (ws) {
ws.removeAllListeners();
if (ws.readyState === ws.OPEN) ws.close();
}
console.log(`Connecting to ${WS_SERVER} as ${IDENTIFIER}...`);
ws = new WebSocket(`ws://${WS_SERVER}`);
ws.on('open', () => {
console.log('Connected, registering...');
ws.send(JSON.stringify({
type: 'register',
identifier: IDENTIFIER
}));
});
ws.on('message', (message) => {
const msg = JSON.parse(message);
if (msg.type === 'registered') {
assignedPort = msg.port;
console.log(`Registered! Forwarding port ${assignedPort} to SSH`);
}
else if (msg.type === 'data') {
if (!sshSocket) connectSSH();
sshSocket.write(Buffer.from(msg.data, 'base64'));
}
});
ws.on('close', () => {
console.log('Disconnected from server');
if (sshSocket) sshSocket.end();
scheduleReconnect();
});
ws.on('error', (err) => {
console.error('WebSocket error:', err);
scheduleReconnect();
});
}
function connectSSH() {
if (sshSocket) sshSocket.end();
sshSocket = new net.Socket();
sshSocket.connect(SSH_PORT, '127.0.0.1', () => {
console.log('Connected to SSH server');
});
sshSocket.on('data', (data) => {
if (ws && ws.readyState === ws.OPEN) {
ws.send(JSON.stringify({
type: 'data',
identifier: IDENTIFIER,
data: data.toString('base64')
}));
}
});
sshSocket.on('error', (err) => {
console.error('SSH error:', err);
sshSocket = null;
});
sshSocket.on('close', () => {
sshSocket = null;
});
}
function scheduleReconnect() {
if (!reconnectTimer) {
console.log(`Reconnecting in ${RECONNECT_INTERVAL/1000} seconds...`);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connectWebSocket();
}, RECONNECT_INTERVAL);
}
}
// Cleanup
function cleanup() {
if (reconnectTimer) clearTimeout(reconnectTimer);
if (ws) ws.close();
if (sshSocket) sshSocket.end();
}
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Start connection
connectWebSocket();
const WebSocket = require('ws');
const net = require('net');
const process = require('process');
// Configuration
const WS_HOST = process.argv[2] || '127.0.0.1:8090';
const [HOST, PORT] = WS_HOST.split(':');
const START_PORT = 9022;
// Client tracking
const clients = new Map(); // identifier -> { ws, port, server, socket, lastActive }
const portAllocation = new Map(); // port -> identifier
const PORT_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours
const wss = new WebSocket.Server({ host: HOST, port: PORT });
console.log(`WebSocket server running on ws://${HOST}:${PORT}`);
// Cleanup old ports
setInterval(() => {
const now = Date.now();
for (const [identifier, client] of clients.entries()) {
if (now - client.lastActive > PORT_TIMEOUT) {
console.log(`Releasing port ${client.port} from ${identifier} (inactive 24h)`);
client.server.close();
portAllocation.delete(client.port);
clients.delete(identifier);
}
}
}, 60 * 60 * 1000); // Check hourly
wss.on('connection', (ws) => {
ws.on('message', (message) => {
try {
const msg = JSON.parse(message);
// Handle registration
if (msg.type === 'register' && msg.identifier) {
registerClient(ws, msg.identifier);
return;
}
// Handle data forwarding
const client = clients.get(msg.identifier);
if (client && client.ws === ws) {
client.lastActive = Date.now();
if (msg.type === 'data' && client.socket) {
client.socket.write(Buffer.from(msg.data, 'base64'));
}
}
} catch (err) {
console.error('Message processing error:', err);
}
});
ws.on('close', () => {
for (const [identifier, client] of clients.entries()) {
if (client.ws === ws) {
console.log(`Client ${identifier} disconnected`);
// Don't close the server, just mark WS as disconnected and close socket
client.ws = null;
if (client.socket) {
client.socket.end();
client.socket = null;
}
}
}
});
});
function registerClient(ws, identifier) {
// Check if this identifier already has a port assigned
const existingClient = clients.get(identifier);
if (existingClient) {
// Reuse the existing port
existingClient.ws = ws;
existingClient.lastActive = Date.now();
console.log(`[${identifier}] Reconnected, using existing port ${existingClient.port}`);
ws.send(JSON.stringify({ type: 'registered', port: existingClient.port }));
return;
}
// Find available port for new client
let port = START_PORT;
while (portAllocation.has(port)) port++;
const server = net.createServer((socket) => {
const client = clients.get(identifier);
if (client) {
client.socket = socket;
console.log(`[${identifier}] New connection on port ${port}`);
}
socket.on('data', (data) => {
const client = clients.get(identifier);
if (client && client.ws) {
client.ws.send(JSON.stringify({
type: 'data',
identifier,
data: data.toString('base64')
}));
}
});
socket.on('close', () => {
const client = clients.get(identifier);
if (client) {
client.socket = null;
}
});
});
server.listen(port, '0.0.0.0', () => {
clients.set(identifier, { ws, port, server, socket: null, lastActive: Date.now() });
portAllocation.set(port, identifier);
console.log(`[${identifier}] Assigned port ${port}`);
ws.send(JSON.stringify({ type: 'registered', port }));
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment