Skip to content

Instantly share code, notes, and snippets.

@vietor
Last active February 4, 2024 05:53
Show Gist options
  • Save vietor/a39ba1d7f672253420d3eacab7a33f74 to your computer and use it in GitHub Desktop.
Save vietor/a39ba1d7f672253420d3eacab7a33f74 to your computer and use it in GitHub Desktop.
ogrok introspectable tunnels to localhost
const net = require('net');
const IdMaker = (function() {
var seq = 0;
return {
next: () => {
seq = (++seq) % 10000;
return Date.now() + '-' + seq;
}
};
})();
function OGQueue() {
const kvdatas = {};
this.push = function(key, value) {
kvdatas[key] = value;
};
this.pop = function() {
let keys = Object.keys(kvdatas);
if (keys.length < 1)
return null;
let key = keys[0];
let value = kvdatas[key];
delete kvdatas[key];
return value;
};
this.delete = function(key) {
delete kvdatas[key];
};
this.size = function() {
return Object.keys(kvdatas).length;
};
}
function OGrokServer(options, serviceHost, servicePortList, tunnelHost, tunnelPort) {
function MySocket(tunnel, handleClose) {
this.sid = IdMaker.next();
var service = null;
var isTunnelClose = false;
var isServiceClose = false;
const onTunnelData = (data) => {
if (Buffer.compare(data, options.kSafeKey) != 0)
tunnel.destroy(new Error('bad safe key'));
};
const onTunnelError = (err) => {
tunnel.destroy(err);
};
const onTunnelClose = (err) => {
if (isTunnelClose)
return;
isTunnelClose = true;
if (service && !isServiceClose)
service.destroy();
handleClose(this.sid);
};
const onServiceError = (err) => {
service.destroy(err);
};
const onServiceClose = (err) => {
if (isServiceClose)
return;
isServiceClose = true;
if (!isTunnelClose)
tunnel.destroy();
};
tunnel.on('error', onTunnelError);
tunnel.on('close', onTunnelClose);
tunnel.once('data', onTunnelData);
this.bridge = function(sock) {
service = sock;
service.on('error', onServiceError);
service.on('close', onServiceClose);
let portBuffer = Buffer.allocUnsafe(4);
portBuffer.writeInt32LE(sock.localPort);
tunnel.write(portBuffer);
tunnel.pipe(service);
service.pipe(tunnel);
};
}
const waitingTunnels = new OGQueue();
const acceptTunnelClient = (client) => {
client.setNoDelay();
client.setKeepAlive(true, options.kKeepAlive);
let ogsock = new MySocket(client, waitingTunnels.delete);
waitingTunnels.push(ogsock.sid, ogsock);
};
const acceptServiceClient = (client) => {
let ogsock = waitingTunnels.pop();
if (!ogsock)
client.destroy();
else
ogsock.bridge(client);
};
this.start = function() {
const tunnelServer = net.createServer(acceptTunnelClient);
tunnelServer.listen(tunnelPort, tunnelHost, () => {
console.log('Start tunnel server on %s:%d (%s)', tunnelHost, tunnelPort, options.kSafeKey);
});
servicePortList.forEach((servicePort) => {
const serviceServer = net.createServer(acceptServiceClient);
serviceServer.listen(servicePort, serviceHost, () => {
console.log('Start service server on %s:%d', serviceHost, servicePort);
});
});
};
}
function OGrokClient(options, tunnelHost, tunnelPort, targetHost, targetPortMap) {
function MySocket(handleClose) {
this.sid = IdMaker.next();
var tunnel = null;
var target = null;
var wakeupData = null;
var isTunnelClose = false;
var isTargetClose = false;
const onTunnelData = (data) => {
if (data.length < 4) {
tunnel.destroy();
return;
}
tunnel.pause();
if (data.length > 4)
wakeupData = data.slice(4);
let targetPort = data.readInt32LE();
if (data.length > 4)
wakeupData = data.slice(4);
if (targetPortMap[targetPort])
targetPort = targetPortMap[targetPort];
else if (targetPortMap["*"])
targetPort = targetPortMap["*"];
target = net.connect(targetPort, targetHost);
target.on('connect', onTargetReady);
target.on('error', onTargetError);
target.on('close', onTargetClose);
};
const onTunnelError = (err) => {
tunnel.destroy(err);
};
const onTunnelClose = (err) => {
if (isTunnelClose)
return;
isTunnelClose = true;
if (target && !isTargetClose)
target.destroy();
handleClose(this.sid);
};
const onTargetReady = () => {
const bridge = () => {
wakeupData = null;
tunnel.resume();
tunnel.pipe(target);
target.pipe(tunnel);
};
if (!Buffer.isBuffer(wakeupData))
bridge();
else
target.write(wakeupData, bridge);
};
const onTargetError = (err) => {
target.destroy(err);
};
const onTargetClose = (err) => {
if (isTargetClose)
return;
isTargetClose = true;
if (!isTunnelClose)
tunnel.destroy();
};
tunnel = net.connect(tunnelPort, tunnelHost, () => {
tunnel.setNoDelay();
tunnel.setKeepAlive(true, options.kKeepAlive);
tunnel.write(options.kSafeKey);
});
tunnel.on('error', onTunnelError);
tunnel.on('close', onTunnelClose);
tunnel.once('data', onTunnelData);
}
const waitingTunnels = new OGQueue();
function idleTimer() {
while (waitingTunnels.size() < options.kClientPool) {
let ogsock = new MySocket(waitingTunnels.delete);
waitingTunnels.push(ogsock.sid, ogsock);
}
}
this.start = function() {
setInterval(idleTimer, options.kClientInterval);
console.log('Start client on %s:%d -> %s:%j (%s)', tunnelHost, tunnelPort, targetHost, targetPortMap, options.kSafeKey);
};
}
function ArgParser(argv) {
function Usage() {
const message = "Usage:\r\n" +
" [options] server <service port1,port2... list> <tunnel port> [bind host]\r\n" +
" [options] client <tunnel host> <tunnel port> [target port1:port1,port2:port2... map] [target host]\r\n" +
"Options:\r\n" +
" --key=<safe key>\r\n" +
" Client only:\r\n" +
" --pool=<pool size, default 100>\r\n" +
" --interval=<reconect interval millseconds, default 333>\r\n";
console.log(message);
process.exit(1);
}
var skip = 0,
options = {};
while (skip < argv.length) {
let value = argv[skip];
if (value.substring(0, 2) != "--")
break;
value = value.substring(2);
if (value) {
let pos = value.indexOf('=');
if (pos < 0)
options[value] = true;
else if (pos > 0)
options[value.substring(0, pos)] = value.substring(pos + 1) || true;
}
skip += 1;
}
if (skip > 0) {
argv = argv.slice(skip);
}
if (argv.length < 1)
Usage();
if (argv[0] != 'server' && argv[0] != 'client')
Usage();
var args = {
type: argv[0]
};
if (args.type == 'server') {
if (argv.length < 3)
Usage();
args.serviceHost = argv[3] || "0.0.0.0";
args.servicePortList = [];
argv[1].split(",").forEach((port) => {
if (port)
args.servicePortList.push(parseInt(port));
});
args.tunnelHost = argv[3] || "0.0.0.0";
args.tunnelPort = parseInt(argv[2]);
} else {
if (argv.length < 3)
Usage();
args.tunnelHost = argv[1];
args.tunnelPort = parseInt(argv[2]);
args.targetHost = argv[4] || "127.0.0.1";
args.targetPortMap = {};
if (argv.length > 3) {
argv[3].split(",").forEach((pair) => {
let ports = pair.split(":");
if (ports.length == 1)
args.targetPortMap["*"] = parseInt(ports[0]);
else if (ports.length > 1)
args.targetPortMap[ports[0]] = parseInt(ports[1]);
});
}
}
return Object.assign(args, {
options: {
kKeepAlive: 10 * 1000,
kSafeKey: Buffer.from("-ogrok2-" + (options.key || "1") + "-channel-"),
kClientPool: parseInt(options.pool || "100"),
kClientInterval: parseInt(options.interval || "333")
}
});
}
const args = ArgParser(process.argv.slice(2));
if (args.type == 'server') {
let ogServer = new OGrokServer(args.options, args.serviceHost, args.servicePortList, args.tunnelHost, args.tunnelPort);
ogServer.start();
} else {
let ogClient = new OGrokClient(args.options, args.tunnelHost, args.tunnelPort, args.targetHost, args.targetPortMap);
ogClient.start();
}
@vietor
Copy link
Author

vietor commented Apr 25, 2019

Demo by pm2 config files

client

{
    "apps": [
        {
            "name": "ogrok-git",
            "script": "./ogrok.js",
            "cwd": "/home/vietor/tasks/ogrok",
            "args": "--key=<safe key> client <host> 14081"
        }
    ]
}

server

{
    "apps": [
        {
            "name": "ogrok-git",
            "script": "./ogrok.js",
            "cwd": "/home/vietor/tasks/ogrok",
            "args": "--key=<safe key> server 4081 14081"
        }
    ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment