newspaint

Documenting Problems That Were Difficult To Find The Answer To

Sharing Port 443 With HTTPS and SSH Using Node.JS

I want to use port 443 for both a HTTPS web server and SSH. This blog post gave the technical details – which is that SSH and HTTPS can be differentiated by the fact that a HTTPS client sends traffic immediately after connecting, whereas SSH clients wait for the SSH banner to be produced by the SSH server.

So I present, here, a TCP proxy that listens on the external IP (whatever your eth0 interface thinks its address is) and then intelligently switches traffic to localhost ports 443 or 22 (configurable) depending on whether it thinks it is HTTPS or SSH.

Note that, because this is a proxy, your SSH and HTTPS logs will not record the source address of who connected to your services – rather the localhost address (127.0.0.1 or ::1) will be shown. See the blog link above for a low-level tool that handles this situation properly.

// import required modules
var net = require('net');

// global variables (defaults)
var debugging = 0;
var internal_ssh_port = 22;
var internal_ssl_port = 443;
var external_port = 443;
var external_address = "0.0.0.0";
var internal_address = "127.0.0.1";
var is_ssh_timeout = 2500; // milliseconds to wait before assuming SSH
var socket_terminate_delay = 500; // milliseconds before destroying end'd sock

// generic function for destroying a socket
function destroySocket( socket, delay, comment ) {
  setTimeout(
    function () {
      try {
        socket.destroy();
      } catch ( err ) {
        if ( debugging ) {
          console.log( '  - error destroying socket "' + comment + '": ' + err );
        }
      }
    },
    delay
  );
}

// generic function for creating a proxied connection
function createSocketProxy( socketServer, type, address, port ) {
  if ( debugging ) {
    console.log( '  > opening ' + type + ' connection to ' + address + ':' + port );
  }

  // create socket and connect to back-end server
  var socketProxy = new net.Socket();
  socketProxy.connect(
    port,
    address,
    function () {
      if ( debugging ) {
        console.log( '  > proxied to ' + type );
      }
    }
  );

  // handle data from the server
  var bytesReceived = 0;
  socketProxy.on(
    'data',
    function ( chunk ) {
      if ( debugging ) {
        bytesReceived += chunk.length;
        console.log( '  < ' + chunk.length + ' ' + type + ' bytes (' + bytesReceived + ' total)' );
      }
      socketServer.write( chunk );
    }
  );

  // handle socket close by the server
  socketProxy.on(
    'end',
    function () {
      if ( debugging ) {
        console.log( '  < ' + type + ' close' );
      }
      var emptyPkt = '';
      socketServer.end( emptyPkt );
      // destroySocket( socketServer, socket_terminate_delay, 'server ' + type );
      socketProxy.end( emptyPkt );
      // destroySocket( socketProxy, socket_terminate_delay, 'proxy ' + type );
      socketProxy = undefined;
    }
  );

  // handle socket error from the server
  socketProxy.on(
    'error',
    function ( err ) {
      if ( debugging ) {
        console.log( '  < ' + type + ' error: ' + err );
      }
      socketServer.end();
      destroySocket( socketServer, socket_terminate_delay, 'server ' + type );
      if ( socketProxy ) {
        socketProxy.destroy(); // sometimes it is undef
      }
      socketProxy = undefined;
    }
  );

  // return the newly created socket
  return( socketProxy );
}

// function for handling connection from client
function socketCallback( socketServer ) {
  var clientSentTraffic = false;
  var socketProxy = undefined;

  if ( debugging ) {
    console.log( "- received connection from " + socketServer.remoteAddress + ":" + socketServer.remotePort );
  }

  // we want to set a timeout here
  setTimeout(
    function() {
      if ( ! clientSentTraffic ) {
        if ( debugging ) {
          console.log( "  > timeout, switching to SSH mode" );
        }
        socketProxy = createSocketProxy(
          socketServer,
          'SSH',
          internal_address,
          internal_ssh_port
        );
      } else {
        if ( debugging ) {
          console.log( "  > ignoring timeout" );
        }
      }
    },
    is_ssh_timeout
  );

  // handle data from client
  socketServer.on(
    'data',
    function( chunk ) {
      console.log( '  > ' + chunk.length + ' bytes' );

      // have we determined yet what kind of connection this is?
      if ( ! socketProxy ) {
        clientSentTraffic = true;

        socketProxy = createSocketProxy(
          socketServer,
          'SSL',
          internal_address,
          internal_ssl_port
        );
      }

      if ( socketProxy )
        socketProxy.write( chunk );
    }
  );

  // handle socket close by client
  socketServer.on(
    'end',
    function() {
      if ( debugging ) {
        console.log( '  > socket close' );
      }

      if ( socketProxy ) {
        socketProxy.end();
        //destroySocket( socketProxy, socket_terminate_delay, 'proxy' );
        socketProxy = undefined;
      }
      socketServer.end();
      //destroySocket( socketServer, socket_terminate_delay, 'server' );
    }
  );

  // handle socket error from client
  socketServer.on(
    'error',
    function( err ) {
      if ( debugging ) {
        console.log( '  > socket error: ' + err );
      }

      if ( socketProxy ) {
        socketProxy.end();
        destroySocket( socketProxy, socket_terminate_delay, 'proxy' );
        socketProxy = undefined;
      }
      if ( socketServer ) {
        socketServer.destroy();
      }
    }
  );
}

function main() {
  // check for any command line arguments
  if ( process.argv.length < 3 ) {
    console.log(
      'ssl/ssh switcher\n' +
      '\n' +
      'Arguments:\n' +
      '  -x <ip> - external interface IP address (default ' + external_address + ')\n' +
      '  -i <ip> - internal interface IP address (default ' + internal_address + ')\n' +
      '  -s <port> - internal SSH port number (default ' + internal_ssh_port + ')\n' +
      '  -w <port> - internal HTTPS port number (default ' + internal_ssl_port + ')\n' +
      '  -p <port> - external port to switch (default ' + external_port + ')\n' +
      '  -d - turn debugging on\n'
    );
    process.exit( 1 );
  }

  for ( var argn = 2; argn < process.argv.length; argn++ ) {
    if ( process.argv[argn] === '-p' ) {
      external_port = parseInt( process.argv[argn + 1] );
      argn++;
      continue;
    }

    if ( process.argv[argn] === '-s' ) {
      internal_ssh_port = parseInt( process.argv[argn + 1] );
      argn++;
      continue;
    }

    if ( process.argv[argn] === '-w' ) {
      internal_ssl_port = parseInt( process.argv[argn + 1] );
      argn++;
      continue;
    }

    if ( process.argv[argn] === '-x' ) {
      external_address = process.argv[argn + 1];
      argn++;
      continue;
    }

    if ( process.argv[argn] === '-i' ) {
      internal_address = process.argv[argn + 1];
      argn++;
      continue;
    }

    if ( process.argv[argn] === '-d' ) {
      debugging = 1;
      continue;
    }
  }

  if ( debugging ) {
    console.log( 'server listening on ' + external_address + ':' + external_port );
  }

  // start TCP server with custom callback function
  var server = net.createServer( socketCallback );
  server.on(
    'error',
    function ( err ) {
      console.log( 'Error with server: ' + err );
    }
  );
  server.listen( external_port, external_address );
}

main();

When run without any command-line arguments you will get the help text:

root@myserver:~# /usr/local/node/bin/node sslsshproxy.js
ssl/ssh switcher

Arguments:
  -x <ip> - external interface IP address (default 0.0.0.0)
  -i <ip> - internal interface IP address (default 127.0.0.1)
  -s <port> - internal SSH port number (default 22)
  -w <port> - internal HTTPS port number (default 443)
  -p <port> - external port to switch (default 443)
  -d - turn debugging on

At a minimum you should specify the -x option with the IP address of your interface. You will find this by running the following command:

root@myserver:~# ifconfig eth0
eth0      Link encap:Ethernet  HWaddr b8:dc:7f:89:2a:a0  
          inet addr:192.168.1.48  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: 2a02:ac8:1:3500::3/64 Scope:Global
          inet6 addr: fe80::bbdd:6fff:fe89:4c1a/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:4223010 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1418451 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:4455158325 (4.4 GB)  TX bytes:466186918 (466.1 MB)
          Interrupt:16 Memory:da000000-da012800 

Then use this in the command:

setsid node sslsshproxy.js -x 192.168.1.48 2>/dev/null >/dev/null &

One response to “Sharing Port 443 With HTTPS and SSH Using Node.JS

  1. Marcel December 15, 2013 at 12:39 pm

    Cool!
    If you add “&& !socketProxy.destroyed” on line 147 it’s stable for ssl connections.

    Kind regards, Marcel

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: