The last time Hackerfall tried to access this page, it returned a not found error. A cached version of the page is below, or click here to continue anyway

Tunnelling TCP/SSH/TLS: Or, the Wonders of stunnel and How to Get Free Airline Wi-Fi :: Potatoblog Kevin Liu's sporadically-updated webspace

[TL;DR: Set up stunnel on a server and use SNI to bypass web filter.]

I recently flew to California, which was fun and exciting! (That is not the focus of this blog post.) Unfortunately, on the return journey, I had an overnight flight with two connections and no free Wi-Fi. Going to any site brings you to a page, requesting $12 for an hour’s connection.

Long story short, this entry will show you that it is possible to get free Wi-Fi on some Viasat flights with only a few exotic technologies and a server back home. (You may also want to try GoGo In-flight or Lifehacker.)

As with any restricted Wi-Fi portal, I began by probing the boundaries. I had ample time to do so on the flight to San Francisco, but unfortunately could not set anything up until the return journey.

So, in summary:

Given these results, I first tried the simple approach of setting up an SSH server on port 21, which should theoretically be allowed. Unfortunately, on my next flight, I found that the connection was reset as soon as the filters detected encrypted, non-FTP data. There goes 2 hours.

I went for an alternative: setting up stunnel, a daemon that lets you tunnel any plaintext protocol over TLS, on port 443 of my web server.

First, a quick preamble on Transport Level Security and how it indicates hostnames. (Skip if you know this already.)

Server Name Indication1 2

Once upon a time, TLS (the protocol that HTTPS uses to secure web traffic) could not host multiple servers on the same IP address. This kind of sucked. The client said Hello, and the server immediately responded with details to set up a secure connection to the only server it hosted.

To solve this, Server Name Indication was introduced. The client said Hello (oh and by the way { server_name: "" }), and the server said Hello, here's Google. This way, the same server could host multiple sites, and everyone was happy. (Well, except Windows XP.)

Note the ordering here: the server name is sent unencrypted, before a connection is set up. It does this because TLS encryption uses a server certificate, which is specific to each host and includes the host name, so obviously you could not send the certificate before knowing the host name3. (I also abuse this property in my famed post about sniproxy.)

This key property allows for TLS filtering: you can check the server_name extension when some Untrusted Client makes a connection, and if it doesn’t match, reset the connection. Simple enough.

…Except, what if I point to some random IP and say I’m trying to access

$ curl --resolve -v -I           
* Added to DNS cache
* Hostname was found in DNS cache
*   Trying
* Connected to ( port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.;
*  start date: Jan 28 00:00:00 2019 GMT
*  expire date: Feb  1 12:00:00 2021 GMT
*  subjectAltName does not match
* SSL: no alternative certificate subject name matches target host name ''
* Closing connection 0
* TLSv1.2 (OUT), TLS alert, close notify (256):
curl: (60) SSL: no alternative certificate subject name matches target host name ''
More details here:

Bingo. (The insecure certificate means curl successfully established a connection.) Since the filter can’t check anything past the initial handshake (it’s all encrypted), and it doesn’t have a whitelist of IPs, it can’t tell that your connection is actually going to CloudFlare in this case.

Now, to leverage this into unrestricted Internet access.

stunnel and Tunnelling over SSH

Enter stunnel, a proxy tool designed to encrypt any connection on any port. For our use-case, we can tunnel SSH over port 443 to appear that we are accessing Viasat while actually connecting to our own server. (If you are just looking for Internet access, you could also tunnel a VPN protocol like OpenVPN or WireGuard.)

The server config file looks like this (coded on NixOS <3):

services.stunnel = {
  enable = true;
  servers.ssh = {
    accept = 2222;
    connect = 743;
    cert = ../secrets/stunnel.pem;

Aka, “tunnel any TLS connection on port 2222 to unencrypted port 743” (my SSH server). You’ll need to generate a self-signed TLS certificate too, which you can easily do with openssl.

I also configured sniproxy to forward any requests to stunnel, but this may not be necessary depending on your setup.

On the client, it looks something like this:

client = yes
foreground = yes
accept =
connect =
sni =

Aka, be a client and forward any connections from port 2222 on localhost to IP (my homelab), port 443. Use a server name of (experiment with others if this doesn’t work for you).

EDIT: By popular demand, here’s an explainer image I made in 5 minutes with Krita:

Once the server and client are running, it’s a simple matter of ssh -D localhost:2222 to start a SOCKS5 proxy from SSH. Then point your browser’s proxy settings to localhost:5000, and voil! Unrestricted internet. I got a surprising ~24 Mbps.

(Any questions or other protocol layering violations to share? Leave a comment!)

Continue reading on