v1.3.0: add inverse ssh proxy

This commit is contained in:
AJ ONeal 2018-09-11 01:51:42 -06:00
parent eba2b4e5b2
commit 0361e5762d
4 changed files with 93 additions and 22 deletions

View File

@ -55,8 +55,9 @@ sclient [flags] <remote> <local>
``` ```
* flags * flags
* -k, --insecure ignore invalid TLS (SSL/HTTPS) certificates * `-k, --insecure` ignore invalid TLS (SSL/HTTPS) certificates
* --servername <string> spoof SNI (to disable use IP as &lt;remote&gt; and do not use this option) * `--servername <string>` spoof SNI (to disable use IP as &lt;remote&gt; and do not use this option)
* `--ssh proxy` proxy ssh over https (_inverse_ ssh proxy, like stunnel)
* remote * remote
* must have servername (i.e. example.com) * must have servername (i.e. example.com)
* port is optional (default is 443) * port is optional (default is 443)
@ -85,7 +86,7 @@ Ignore a bad TLS/SSL/HTTPS certificate and connect anyway.
sclient -k badtls.telebit.cloud:443 localhost:3000 sclient -k badtls.telebit.cloud:443 localhost:3000
``` ```
Reading from stdin ### Reading from stdin
```bash ```bash
sclient telebit.cloud:443 - sclient telebit.cloud:443 -
@ -95,7 +96,13 @@ sclient telebit.cloud:443 -
sclient telebit.cloud:443 - </path/to/file sclient telebit.cloud:443 - </path/to/file
``` ```
Piping ### ssh over https
```bash
sclient --ssh user@telebit.cloud
```
### Piping
```bash ```bash
printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443 printf "GET / HTTP/1.1\r\nHost: telebit.cloud\r\n\r\n" | sclient telebit.cloud:443

View File

@ -8,6 +8,7 @@ var localAddress;
var localPort; var localPort;
var rejectUnauthorized; var rejectUnauthorized;
var servername; var servername;
var sshProxy;
function usage() { function usage() {
console.info(""); console.info("");
@ -38,6 +39,14 @@ function parseFlags() {
return true; return true;
} }
}); });
process.argv.some(function (arg, i) {
if (/^--?ssh$/.test(arg)) {
sshProxy = true;
process.argv.splice(i, 1);
process.argv[3] = '$';
return true;
}
});
} }
parseFlags(); parseFlags();
@ -45,6 +54,15 @@ parseFlags();
remote = (process.argv[2]||'').split(':'); remote = (process.argv[2]||'').split(':');
local = (process.argv[3]||'').split(':'); local = (process.argv[3]||'').split(':');
if ('ssh' === remote[0]) {
sshProxy = true;
remote = local;
local = ['$'];
} else if ('ssh' === local[0]) {
sshProxy = true;
local[0] = '$';
}
// arg 0 is node // arg 0 is node
// arg 1 is sclient // arg 1 is sclient
// arg 2 is remote // arg 2 is remote
@ -72,23 +90,57 @@ if (local[0] === String(parseInt(local[0], 10))) {
localPort = parseInt(local[0], 10); localPort = parseInt(local[0], 10);
localAddress = 'localhost'; localAddress = 'localhost';
} else { } else {
localAddress = local[0]; // potentially '-' or '|' localAddress = local[0]; // potentially '-' or '|' or '$'
localPort = parseInt(local[1], 10); localPort = parseInt(local[1], 10);
} }
if ('-' === localAddress || '|' === localAddress) { var rparts = remote[0].split('@');
// no need for port var username = rparts[1] ? (rparts[0] + '@') : '';
} else if (!localPort) {
usage();
return;
}
var opts = { var opts = {
remoteAddr: remote[0] remoteAddr: rparts[1] || rparts[0]
, remotePort: remote[1] || 443 , remotePort: remote[1] || 443
, localAddress: localAddress , localAddress: localAddress
, localPort: localPort , localPort: localPort
, rejectUnauthorized: rejectUnauthorized , rejectUnauthorized: rejectUnauthorized
, servername: servername , servername: servername
, stdin: null
, stdout: null
}; };
require('../')(opts);
if ('-' === localAddress || '|' === localAddress) {
opts.stdin = process.stdin;
opts.stdout = process.stdout;
// no need for port
} else if ('$' === localAddress) {
sshProxy = true;
opts.localAddress = 'localhost';
opts.localPort = 0; // choose at random
} else if (!localPort) {
usage();
return;
}
var emitter = require('../')(opts);
emitter.once('listening', function (opts) {
var port = opts.localPort;
console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort
+ " <= " + opts.localAddress + ":" + opts.localPort);
if (sshProxy) {
// TODO choose at random and connect to ssh after test
var spawn = require('child_process').spawn;
var ssh = spawn('ssh', [
username + 'localhost'
, '-p', port
// we're _inverse_ proxying ssh, so we must alias the serveranem and ignore the IP
, '-o', 'HostKeyAlias=' + opts.remoteAddr
, '-o', 'CheckHostIP=no'
], { stdio: 'inherit' });
ssh.on('exit', function () {
console.info('shutting down...');
});
ssh.on('close', function () {
opts.server.close();
});
}
});

View File

@ -3,7 +3,7 @@
var net = require('net'); var net = require('net');
var tls = require('tls'); var tls = require('tls');
function listenForConns(opts) { function listenForConns(emitter, opts) {
function pipeConn(c, out) { function pipeConn(c, out) {
var sclient = tls.connect({ var sclient = tls.connect({
servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort servername: opts.remoteAddr, host: opts.remoteAddr, port: opts.remotePort
@ -28,7 +28,7 @@ function listenForConns(opts) {
} }
if ('-' === opts.localAddress || '|' === opts.localAddress) { if ('-' === opts.localAddress || '|' === opts.localAddress) {
pipeConn(process.stdin, process.stdout); pipeConn(opts.stdin, opts.stdout);
return; return;
} }
@ -40,12 +40,15 @@ function listenForConns(opts) {
host: opts.localAddress host: opts.localAddress
, port: opts.localPort , port: opts.localPort
}, function () { }, function () {
console.info('[listening] ' + opts.remoteAddr + ":" + opts.remotePort opts.localPort = this.address().port;
+ " <= " + opts.localAddress + ":" + opts.localPort); opts.server = this;
emitter.emit('listening', opts);
}); });
} }
function testConn(opts) { function testConn(opts) {
var emitter = new (require('events').EventEmitter)();
// Test connection first // Test connection first
var tlsOpts = { var tlsOpts = {
host: opts.remoteAddr, port: opts.remotePort host: opts.remoteAddr, port: opts.remotePort
@ -53,15 +56,24 @@ function testConn(opts) {
}; };
if (opts.servername) { if (opts.servername) {
tlsOpts.servername = opts.servername; tlsOpts.servername = opts.servername;
} else if (/^[\w\.\-]+\.[a-z]{2,}$/i.test(opts.remoteAddr)) {
tlsOpts.servername = opts.remoteAddr.toLowerCase();
}
if (opts.alpn) {
tlsOpts.ALPNProtocols = [ 'http', 'h2' ];
} }
var tlsSock = tls.connect(tlsOpts, function () { var tlsSock = tls.connect(tlsOpts, function () {
setTimeout(function () {
tlsSock.end(); tlsSock.end();
listenForConns(opts); listenForConns(emitter, opts);
}, 200);
}); });
tlsSock.on('error', function (err) { tlsSock.on('error', function (err) {
console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n'); console.warn("[warn] '" + opts.remoteAddr + ":" + opts.remotePort + "' may not be accepting connections: ", err.toString(), '\n');
listenForConns(opts); listenForConns(emitter, opts);
}); });
return emitter;
} }
module.exports = testConn; module.exports = testConn;

View File

@ -1,6 +1,6 @@
{ {
"name": "sclient", "name": "sclient",
"version": "1.2.2", "version": "1.3.0",
"description": "Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. Also ideal for multiplexing a single port with multiple protocols using SNI.", "description": "Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. Also ideal for multiplexing a single port with multiple protocols using SNI.",
"main": "index.js", "main": "index.js",
"homepage": "https://telebit.cloud/sclient/", "homepage": "https://telebit.cloud/sclient/",