Piercing firewalls with sniproxy, stunnel, OpenVPN and a Raspberry Pi

In this blog post I’ll show you how to get a VPN-connection up and running through (almost) any firewall:

  • A server at home (on an unrestricted line) acts as an OpenVPN-server, while the client is connected to a very restrictive network (e.g. a hotel wifi or some free municipal wifis; but think twice before messing with your employer’s corporate network or even the Great Firewall of China...)
  • OpenVPN-traffic is encapsulated in regular SSL/TLS in order to hide it from intrusive firewalls (doing Deep Packet Inspection).
  • This “stunneled” VPN-connection is exposed to the outside world on port 443 (which is often the only non-blocked port available).
  • Port 443 is still available for regular https-hosting. This is achieved through port-sharing implemented by sniproxy.
  • All of this is implemented on a Raspberry Pi! (Except for the actual VPN-server, but that’s the least interesting part of the whole setup anyway... I’m simply documenting my own setup, feel free to run the openvpn-server on the Pi too.)

An obvious prerequisite is to have a public IP address for your server (i.e. Raspberry Pi). You may use your own domain or open an account at one of the numerous “dynamic DNS” providers. The example setup is for a registered domain called “mydomain.com”. A total of three subdomains are supposed to point to the public IP of the server: one for the VPN service (“vpn.mydomain.com”) and two for various other self-hosted web services (“firstserver.mydomain.com” and “secondserver.mydomain.com”). All of this on the standard SSL-port 443 and a single IP.

Setting up OpenVPN

... is left as an exercise for the reader. The web is full of excellent HOWTOs explaining how to set up OpenVPN on almost any platform. Sometimes it’s as easy as clicking a few buttons (NAS, routers, etc.) For the remainder of this post, I’ll suppose the openvpn-server is listening on an internal server reachable as “vpnserver:1194”. Note however that the VPN must be using TCP for its connections (instead of the default UDP). Technically this is not an optimal VPN configuration, but it’s a requirement for our intended setup.

stunnel

Stunnel is a very useful piece of software: it allows to set up an encrypted SSL/TLS tunnel between two arbitrary endpoints. Most often it’s used to add an encryption layer to non-encrypted server applications. Both server and clients are available for almost any OS and platform. In this setup it’s only used to hide the distinctive OpenVPN-handshake from the restrictive firewall. Even though OpenVPN is actually SSL/TLS-based, its traffic can easily distinguished from true SSL/TLS as used by a webserver, for example. Hence it’s often insufficient to configure OpenVPN to listen on port 443.

The following setup is based on a Raspberry Pi (running Minibian, a stripped-down version of Raspbian). The only minor difference between Raspbian and Minibian is that, by default, Minibian provides a working and active root-account. If you’re on the standard Raspbian distribution, you’ll have to either “sudo -i” or prefix all commands with “sudo”.

apt-get install stunnel4

In order to provide SSL-services, you’ll have to create and install the necessary encryption keys. (This is only needed on the server.) This can be achieved with the following commands:

cd /etc/stunnel
openssl genrsa -out vpn.mydomain.com.key
openssl req -new -key vpn.mydomain.com.key -out vpn.mydomain.com.csr
openssl x509 -req -in vpn.mydomain.com.csr -signkey vpn.mydomain.com.key -out vpn.mydomain.com.crt
cat vpn.mydomain.com.key vpn.mydomain.com.crt > vpn.mydomain.com.pem
chmod 400 /etc/stunnel/vpn.mydomain.com.pem

Now, you’ll have to configure your server-stunnel to listen on some random port (in my case 8194) and forward all incoming SSL-decrypted traffic to your regular OpenVPN server and port. Create a new configuration file /etc/stunnel/tunneledvpn.conf and put the following lines into it:

cert = /etc/stunnel/vpn.mydomain.com.pem
[tunneledvpn]
accept  = 8194
connect = vpnserver:1194

After ENABLEing stunnel in /etc/default/stunnel4 and restarting it (service stunnel4 restart), stunnel should be up and running. On the client device you’ll also have to configure stunnel. For simplicity’s sake, I’ll provide instructions for a Ubuntu desktop.

sudo apt-get install stunnel4
sudo vim /etc/stunnel/tunneledvpn.conf

Here’s the client config:

[tunneledvpn]
client = yes
accept = 8194
connect = vpn.mydomain.com:8194
ciphers = RC4

At this stage, you may try if your OpenVPN works through stunnel. You’ll either have to open up port 8194 on your server or do the test locally. Of course this may be blocked by an overly restrictive firewall which only allows SSL-traffic on port 443. In that case, you could (temporarily) move stunnel to port 443. To run the test, make sure stunnel is started on both the client and server and configure your OpenVPN-client to connect to TCP-port 8194 on the local machine (i.e. localhost!). Remember: OpenVPN has to connect to the clientside endpoint of the stunnel instead of directly to the VPN server.

The last line of the configuration file specifies that the TLS link should only use the outdated and insecure RC4 encryption algorithm. This is a straightforward performance optimisation, RC4 being much faster than the default AES. (The Pi can be challenged by even the most modest tasks, like forwarding TCP packets through sniproxy. Any superfluous processing, such as strong encryption, is to be avoided.) You may be wondering why I’m specifying the cipher in the client config instead of the central server config. The reason is simple: I couldn’t get it to work the other way round... YMMV

sniproxy

The most interesting part is to set all of this up in a way so that it’s still possible to host regular https-websites (even multiple ones!) on port 443. This is made possible through the extensive use of TLS’s “Server Name Indication” (or SNI for short). This TLS-extension (which is supported almost universally) is used by web browsers or other client programs to signal to the TLS-server which domain name it actually wants to contact. This makes it possible to have a virtual server setup (i.e. multiple domains) hosted on a single IP address even when using TLS! In my case, I’m using nginx to host a regular site (this one...) and a seafile server on the same public IP address. Unfortunately it’s not possible (or at least I’m not aware of it) to use nginx to TLS-proxy an OpenVPN-connection. That’s where sniproxy comes into play: It’s a simple TCP proxy for TLS connections. When an SNI-enabled client application connects to a TLS-server it notifies the server in the very first data packet of the intended recipient domain (or subdomain). This allows sniproxy to transparently forward this packet (and all the related ones) to either nginx or stunnel.

Unfortunately, there are no precompiled packages for Raspbian (or at least I couldn’t find any that would actually run). That’s why you’ll have to compile your own. (Alternatively, you could just skip the next few steps and simply download my self-compiled .deb files. You’re welcome ;-))

Setting up a package building environment

The compiler suite and other related “stuff” pulled in a whopping 52.8MB on my raspberry (and used an additional 132MB of disk space after installation).

apt-get install autotools-dev cdbs debhelper dh-autoreconf dpkg-dev gettext libev-dev libpcre3-dev pkg-config fakeroot

Compiling and installing udns

Udns is an optional (but extremely useful) dependency of sniproxy: Without it, you can only specify IP addresses but no hostnames in sniproxy’s config files...

mkdir udns_packaging
cd udns_packaging
wget http://archive.ubuntu.com/ubuntu/pool/universe/u/udns/udns_0.4-1.dsc
wget http://archive.ubuntu.com/ubuntu/pool/universe/u/udns/udns_0.4.orig.tar.gz
wget http://archive.ubuntu.com/ubuntu/pool/universe/u/udns/udns_0.4-1.debian.tar.gz
tar xfz udns_0.4.orig.tar.gz
cd udns-0.4/
tar xfz ../udns_0.4-1.debian.tar.gz
dpkg-buildpackage
cd ..
dpkg -i libudns0_0.4-1_armhf.deb libudns-dev_0.4-1_armhf.deb
cd ..

Here are the compiled packages: libudns0_0.4-1_armhf.deb, libudns-dev_0.4-1_armhf.deb and udns-utils_0.4-1_armhf.deb. (You probably won’t need the utils package.)

Compiling and installing sniproxy

mkdir sniproxy_packaging
cd sniproxy_packaging
wget https://github.com/dlundquist/sniproxy/archive/0.4.0.tar.gz
tar xzf 0.4.0.tar.gz
cd sniproxy-0.4.0
./autogen.sh && dpkg-buildpackage
cd ..
dpkg -i sniproxy_0.4.0_armhf.deb

Here are the compiled packages: sniproxy_0.4.0_armhf.deb and sniproxy-dbg_0.4.0_armhf.deb. (You probably won’t need the debug package.)

Configuring sniproxy

First, we’ll have to tell sniproxy about the various endpoints to which it should forward incoming SSL-traffic. Here’s an example config; it has to be stored as /etc/sniproxy.conf:

user daemon

# PID file
pidfile /var/run/sniproxy.pid

error_log {
    # Log to the daemon syslog facility
    syslog daemon

    # Alternatively we could log to file
    #filename /var/log/sniproxy/sniproxy.log

    # Control the verbosity of the log
    priority notice
}

# Replace LOCALIP by the IP address on which sniproxy should
# listen. I had difficulties setting this up using the hostname:
# sniproxy always ended up listening on its IPv6-address only.
listen LOCALIP:8443 {
    proto tls
    table https_hosts
    bad_requests log
    access_log {
        filename /var/log/sniproxy/https_access.log
        priority notice
    }
}

table https_hosts {
# VPN (proxy vpn packets to the stunneled OpenVPN)
	vpn.mydomain.com	localhost:8194
# nginx virtual servers (must be SSL-enabled!)
	firstserver.mydomain.com   localhost:443
	secondserver.mydomain.com  localhost:443
}

There are only a few final steps left:

  • ENABLE sniproxy by editing /etc/default/sniproxy.
  • Change your router configuration to forward external TCP port 443 to port 8443 on your Raspberry. (I’m assuming that your Raspberry isn't connected directly to the internet, but is behind a router performing Network Address Translation.)
  • Modify the client stunnel to connect to port 443 (instead of the earlier 8194).
  • Restart all the relevant services (just to make sure): client and server stunnel, sniproxy.

You may now test your new configuration: by entering https://firstserver.mydomain.com or https://secondserver.mydomain.com in your browser you should be forwarded automatically to the correct site (just like before the modifications). At the same time it should be possible to access the VPN-server through stunnel.

I hope you’ve found this guide useful. Feel free to let me know your thoughts in the comments.

Tags : firewallsniproxystunnelOpenVPNvpnprivacyminibianraspbian

Copyright © 2015–2024 Hambier
GS RU