TLS/SSL tunneling with Node.js

Posted by by Nick Letts

I was recently faced with a bit of a coding challenge whereby I needed to get LDAP authentication working via SSL/TLS using Node. Unfortunately for me Node.js is a relatively new language and a secure LDAP library is still on the wish list. When I was first given this task, I actually didn’t know where to start. I looked into creating a Node wrapper for some of the OpenLDAP libraries written in C. My project team was already using node-ldapauth, which utilizes OpenLDAP behind the scenes, so extending that was a possibility. I felt though that there must be an easier alternative, especially given how powerful node is with I/O. So I decided to implement a kind of TLS/SSL tunnel/port forward solution and use it in conjunction with node-ldapauth. Node v0.4.7 already has a built-in TLS connection library, so it was just a matter of constructing a ‘tunnel’ with a non-secure socket on one end, and a secure socket on the other.



Before trying anything too fancy, I thought I would prove the concept worked with a basic test. To do this I created three small apps:

A Server (Only accepts secure connections)

The Tunnel

A Client (Only makes non secure connections)

In order to make this a true SSL connection, I first needed to generate a certificate and a private server key. Thankfully OpenSSL makes this task a breeze:

openssl genrsa -out server.key 1024

openssl req -new -key server.key -out csr.pem

openssl x509 -req -in csr.pem -signkey server.key -out cert.pem

Here is the code for each of the apps:

server.js:

var tls = require('tls'), fs = require('fs'), sys = require('sys'); var options = { key: fs.readFileSync('server.key'), cert: fs.readFileSync('cert.pem') }; sys.puts("TLS server started."); tls.createServer(options, function (socket) { sys.puts("TLS connection established"); socket.addListener("data", function (data) { sys.puts("Data received: " + data); }); socket.pipe(socket); }).listen(8000);

tunnel.js:

var tls = require('tls'), fs = require('fs'), sys = require('sys'), net = require('net'); var options = { cert: fs.readFileSync('cert.pem'), ca: fs.readFileSync('cert.pem') }; sys.puts("Tunnel started."); var client = this; // try to connect to the server client.socket = tls.connect(8000, options, function() { if (client.socket.authorized) { sys.puts("Auth success, connected to TLS server"); } else { //Something may be wrong with your certificates sys.puts("Failed to auth TLS connection: "); sys.puts(client.socket.authorizationError); } }); client.socket.addListener("data", function (data) { sys.puts("Data received from server: " + data); }); var server = net.createServer(function (socket) { socket.addListener("connect", function () { sys.puts("Connection from " + socket.remoteAddress); //sync the file descriptors, so that the socket data structures are the same client.socket.fd = socket.fd; //pipe the incoming data from the client directly onto the server client.socket.pipe(socket); //and the response from the server back to the client socket.pipe(client.socket); }); socket.addListener("data", function (data) { sys.puts("Data received from client: " + data); }); socket.addListener("close", function () { //close the tunnel when the client finishes the connection. server.close(); }); }); server.listen(7000);

client.js:

var net = require('net'), sys = require('sys'); var msg = "Hello from net client!"; client = net.createConnection(7000, function() { sys.puts("Sending data: " + msg); client.write(msg); }); client.addListener("data", function (data) { sys.puts("Received: " + data); });

Make sure the certificate and key files are in the same directory as the server/tunnel/client and then start them up in different terminals in the following order:

node server.js node tunnel.js node client.js

If all goes well you should see the text: “Hello from client!” across all terminals.

To get this working with our existing LDAP library, I wrapped the tunnel code in a method ‘connect’ with a callback function, which then invokes the LDAP authenticate code:

tunnel.connect(ldap_port, ldap_host, ssloptions, function() { ldap.authenticate(tunnel.port, etc...) }

The LDAP authenticate call is configured to point to the tunnel, and the tunnel points to the actual LDAP server on the TLS port (636).

I love how easy this task was made, simply by using Node. It’s a powerful language and I look forward to using it in the future.