Introduction

You know the pain - you’re on a Wi-Fi someplace nice that’s configured to block anything other than ports 443 and 80. If only you had an SSH server listening on one of those ports… First, let’s go through a very naive solution and have some fun with it. Then we’ll talk about more reasonable solutions, like the awesome HAProxy trick that let’s you serve both HTTPS and SSH on a single port.

What do we do about it?

We’ll write a tiny Node app that you’ll be able to put on event the smallest of servers, set it to listen on a non-blocked port (usually 80 and 443) and foward all your dirty SSH conversations to a proper SSH server. Then we’ll throw around some ideas on what you could do as the middle-man in an SSH connection.

The base proxy

We want an app that listens on a port like 80 or 443 and forwards all data sent to an SSH server. SSH happens over TCP so basically the only thing we need to do is:

  • Open up a socket on port 80 and start accepting connections
  • Receive all data from that socket and forward it upstream
  • Log each connection to watch the pretty numbers scroll by

We’ll use net for the sockets and debug for pretty-printing stuff. Here’s just enough code to start accepting connections and open a connection upstream in response:

var net = require('net');
var debug = require('debug');
var mainLogger = debug('main');

var UPSTREAM_PORT = 22;
var UPSTREAM_ADDR = 'ssh.exana.org';
var LOCAL_PORT = 80;
var LOCAL_ADDR = '0.0.0.0';

net.createServer(function(sock) {

  // Create a separate logger for this connection.
  var logger = debug(`${sock.remoteAddress}:${sock.remotePort}`);
  logger('** Connected.');

  var upstream = new net.Socket();
  upstream.connect(UPSTREAM_PORT, UPSTREAM_ADDR, function() {
    logger(`** Connected to ${UPSTREAM_ADDR}:${UPSTREAM_PORT}`);
  });
  
}).listen(LOCAL_PORT, LOCAL_ADDR);

mainLogger(`Live! Tune in to ${LOCAL_ADDR}:${LOCAL_PORT}`);

Let’s run this and try connecting with netcat:

$ node proxoxo.js
main Live! Tune in to 0.0.0.0:80 +0ms
127.0.0.1:63079 ** Connected. +4s
127.0.0.1:63079 **  to ...

That’s a start. To make it do the actual proxying, all we need to add is handling data, close and, for the fun of it, error events on both sockets. Handling data is straightforward - just cram it into the other socket with .write(). We’ll also .end() the dowstream socket if upstream closes its connection or errors.

/* ... snip ... */
net.createServer(function(sock) {

  /* ... snip ... */

  /*
   * Remember:
   *   - sock: the connection to the SSH client
   *   - upstream: the connection to the SSH server
   */
 
  upstream.on('data', function(data) {
    logger('<- Upstream sent ' + data.length + ' bytes.');
    sock.write(data);
  });

  upstream.on('close', function(data) {
    logger('** Upstream closed connection.');
    sock.end();
  });

  upstream.on('error', function(err) {
    logger(`!! Ouch, upstream errored: ${err.message}`);
  });

  sock.on('data', function(data) {
    logger(`-> Received ${data.length} bytes...`);
    upstream.write(data);
  });

  sock.on('error', function(err) {
    logger(`!! Oof, clienf errored: ${err.message}`);
  });

  sock.on('close', function(data) {
    logger('** Closing socket: ' + sock.remoteAddress + ':' + sock.remotePort);
  });

  /* ... snip ... */
});

I’m feeling adventurous!

Here two things you can do now that guarantee fun - if your definition of fun is sufficiently skewed:

  • Log and print out (a heatmap, perhaps?) the distribution of bytes send over the connection. How uniform is it? Could you determine the characteristics of an SSH connection and discern it among other traffic? Not to spoil it, but bet you could.
  • Flip random bits and see how SSH copes with it - though it usually wont, unless you hit a few fun cases.

Now for real solutions

If you’ve been following this article, you just built your own, hyper-primordial version of HAProxy. You can drop it on a server of yours, keep using it and no-one would ever know.

If you’re into checking out something you won’t get fired for, though, here’s a list:

  • Configure HAProxy to handle both SSL and SSH traffic on 443 and proxy it accordingly - this is a super sleek solution and you should get it running in the long run.
  • Give sshttp a shot.
  • Just configure sshd to listen in on 80/443 as well.

HAProxy HTTPS/SSH “multiplexing” configuration

From David Leadbeater’s awesome post:

defaults
  timeout connect 5s
  timeout client 50s
  timeout server 20s

listen ssl :443
  tcp-request inspect-delay 2s
  acl is_ssl req_ssl_ver 2:3.1
  tcp-request content accept if is_ssl
  use_backend ssh if !is_ssl
  server www-ssl :444
  timeout client 2h

backend ssh
  mode tcp
  server ssh :22
  timeout server 2h

This listens on port 443, forwards it to port 444 (where the actual SSL web server is listening) unless it is not SSLv2, SSLv3 or TLSv1 traffic, in which case it forwards it to the ssh backend listening on port 22. source

Check the out the original post here, and a neatly insightful discussion in this HN thread.