Making a Local Web Server Public with Localtunnel

JUN 06

This week we thought we’d share some background on localtunnel, a project I wrote outside of Twilio to help deal with the challenges of developing against webhook-based APIs (such as Twilio’s) when coding behind a NAT.

These days it’s fairly common to run a local environment for web development. Whether you’re running Apache, Mongrel, or the App Engine SDK, we’re all starting to see the benefits of having a production-like environment right there on your laptop so you can iteratively code and debug your app without deploying live, or even needing the Internet.

The Problem

With the growing popularity of HTTP callbacks, or webhooks, there are cases where you can really only debug your app while live and on the Internet. Webhooks aside, there are other cases where you might need to make local web servers public, such as testing or public demos. Demos are a surprisingly common case, especially for multi-user systems (“Man, I wish I could have you join this chat room app I’m working on, but it’s only running on my laptop”).

The Solution?

To some, the solution is obvious: SSH tunneling! That is, use a magical set of options with SSH on a hosted box to set up a tunnel from that machine to your local machine. When people connect to a port on your public machine, it gets forwarded to a local port on your machine, looking as if that port was on a public IP.

The idea is great, but it’s a hassle to set up. It’s not just a large, unwieldy command you have to do every time, but you have to make sure sshd is set up properly in order to make a public tunnel on the remote machine. Otherwise you need to set up two tunnels, one from your machine to a private port on the remote machine, and then another on the remote machine from a public port to the private port (that forwards to your machine).

An Easier Solution

I think it’s too much of a hassle to consider SSH tunneling as “a quick and easy option.” Especially if you’re trying to help somebody else set up a tunnel. This is when I started to envision a simple command and service to make this dead simple. Here is the quick and easy way that localtunnel provides:

$ localtunnel 8080

And you’re done! With localtunnel, it’s so simple to set this up, it’s almost fun to do. What’s more is that the publicly accessible URL has a nice hostname and uses port 80, no matter what port it’s on locally. And it tells you what this URL is when you start localtunnel:

$ localtunnel 8080
Port 8080 is now accessible from ...

This URL can now be shared with others, used for webhook callbacks, etc. We assume the tunnel will be fairly short-lived, although we don’t actively close long-lived tunnels. The tunnel will generally be available until you stop the process.

Here’s another example of using localtunnel to make the built-in Python HTTP server public for quickly sharing files over the web:

$ python -m SimpleHTTPServer 8000 &
$ localtunnel 8000
Port 8000 is now accessible from ...

Now you’ll get a directory listing of files in the current directory at that URL. Share the URL and now people can access those files until you close the tunnel.

How It Works

To be clear, this is still at its core SSH tunneling, but wrapped up in a nice package involving a simple client and server. But let’s see what all is happening.

The localtunnel command is written in Ruby and uses an SSH library to open the actual tunnel, but first it hits a tunnel registration API. The API is on the same server that you tunnel through, provided by the server component.

The server component provides two services: a reverse proxy to the forwarded port, and the tunnel registration API. You can see the registration API for yourself, just browse to This simple API allocates an unused port to tunnel on, and gives the localtunnel client the information it needs to set up an SSH tunnel for you.

Of course, there’s also authentication. As a free and public service, I don’t want to just give everybody SSH access to this machine (as it may seem). Instead, since we’re wrapping SSH, we use public keys and per key options to lock down access while still allowing normal SSH access for anything else on the box.

The server runs as a user with no shell. It only has a home directory with an authorized_keys file. The first time you use localtunnel, you have to use the -k option specifying a public key to pass along when we register a tunnel. We verify it’s a valid key and then add it to the authorized_keys file with a bunch of options preventing pretty much any use of SSH other than tunneling to a private port on the server. After that, assuming SSH can find your private key, it just works.

We only allow private port tunneling because we don’t want arbitrary public port forwarding from the server. Public access to this private port on the server comes through our reverse proxy listening on port 80 for any * requests. We keep the mapping of randomly generated hostnames for each active port in memory and use that to determine the backend for our reverse proxy. This backend, however, is actually your local server thanks to the SSH tunnel.

It’s important to consider that right now there are no real privacy guarantees and currently no HTTPS support. This is on the roadmap, but for now this means you may not want to share highly sensitive data over localtunnel.

That’s pretty much it. You can explore further by looking at the code on GitHub. The server component is written in Twisted Python and is just over 100 lines of code.

What Now?

You can start using it immediately if you have Ruby and Rubygems installed. You just need to run:

$ gem install localtunnel

Although it currently depends on Ruby, one contributor is working on a Java client. Speaking of contributors, there are a few other things on the roadmap if anybody wants to help:

  • CNAME support for long-lived or "reserved" tunnels
  • HTTPS support for fully secure tunnels
  • Automatic key generation, eliminating the need to initially specify a key

Otherwise, enjoy the free service and feel free to stand up your own instance!

Posted by Progrium on June 06, 2011