Blocking VPNs is a common practice nowadays, be it on ISP/Government level or just the local coffee shop. Also, if you can hide the information that you are using a VPN, shouldn’t you just do it for the sake of it?


Analyzing the Problem 

Threat models 

Blind blocking of ports (or IPs) 

The most common way of blocking VPN connections - or really any connections of well known services - is by blocking it’s default port(s). In extension, some public WLAN-hotspots may even block all ports except 80 (HTTP) and 443 (HTTPS). Not quite as common but still sometimes observable in public hotspots is the blocking of specific IPs - which may include servers of big VPN providers. However since we will set up our own small VPN server, it is safe to assume that we aren’t in any such blacklist (yet). This brings me to the next point.

Targeted blocking of your server due to information leakage 

Now obviously we are starting to leave the realm of coffee-shop-next-door-blocking. But assuming someone cares enough, may it just be a bored admin, a second threat model is an active attacker that attempts to find out, if a given server provides a VPN, based on information the server or client leaks. Such leaked information would for example be the Server Name Identification (SNI), which could leak from an unencrypted DNS-request or the server’s certificate or the TLS handshake itself), plus the fact that a VPN-service answers on thus leaked sub-domains.

Deep package inspection 

Lastly, even if DPI is a far less common blocking mechanism, it is an inbuilt feature in some commercially available firewalls, and I know of at least one public hotspot in my area which seems to recognize and block vpn connections over ports 80 and 443 (presumably based on DPI). Way more important perhaps: if you are on vacation in for example Turkey, then assuming that the government analyzes your traffic, flag you for using a VPN, block it and/or blacklist your server, might not be so unreasonable. After all people in Turkey have gone to jail for equally ridiculous reasons.

Other things to consider 

Impossibility of SNI Confidentiality 

An important thing to note is that we cannot protect the request SNI as it is in the plain text part of the TLS handshake. This has implication on how our server must differentiate between a normal HTTPS request and VPN connection attempt, so that even an active attacker cannot not identify our server as a VPN provider. We will discuss this problem in a later section.

Abnormalities in browsing behavior 

Since we will route our traffic through the VPN it will look like we are only visiting one site. If we are paranoid we also have to look into a tool that simulates other browsing behavior for us, but I think this will be a topic for another time.

Circumventing VPN blocks might be against the law or at the very least the terms of service of a public Hotspot, while it is basically impossible that any of your VPN activity will come back to haunt the hotspot provider, since the rest of the internet will only see the IP of your server, fear of circumvention might impede the creation of more public WLAN spots.

False sense of anonymity 

It is always important to remember that a VPN is not Tor. Even if you aren’t hosting the VPN endpoint yourself, and even if you trust your VPN provider to not log your data (or at the very least no give it away), a VPN does not protect you Identity reliably. You are still susceptible to browser fingerprinting and information leaks from your software. If you need real anonymity you have to use the tor browser on top or instead of your VPN. The tor browser also has very advanced obfuscation plugins itself.

Hey, actually now that you say it: Why not just use the tor network in the first place? 

You have to remember that end-point anonymity is not our goal at the moment, right now we don’t care if the target Website can track or identify us, we only care to escape our current network and reach our server and cannot be caught using VPN. Tor has mechanisms for this too, but Tor in general tends to be slow. I admire you if you can use Tor in your every day browsing, but I have gotten used to pages loading instantly and not after twenty seconds. However, if true anonymity is what you need, you don’t need a VPN, you need Tor. Perhaps even with a Live-CD to reduce the impact of browser fingerprinting.

Formulating design goals for our stealth VPN 

  1. must use port 443 or 80 for our VPN connection to circumvent port blocking
  2. the VPN traffic must look like HTTPS traffic when analyzed
  3. server, client and connection should not leak any information that would make it look non-standard traffic

Outlining the internal workings 

We will use stunnel on our client to connect to the nginx on our server. Nginx will have to multiplex the connection and either provide normal web-content or stream the connection to our VPN server. As said earlier: We cannot protect the request SNI. Therefore if we want to be completely safe, we cannot multiplex based on the SNI (aka the requested subdomain), as an active attacker would easily be able to tell apart a VPN server from an HTTP server if he attempts to connect to a give SNI. Nevertheless we will do that first in the configuration section below and then build upon, it since this is probably the point where we enter full-scale paranoia territory.

Requirements 

I will be using a Debian 9 server in this example, you will need nginx on your server, stunnel on your client, and, obviously, openvpn on both. Also the nginx version from the main repository and even backports is too old, you will have to use one from the offical nginx repository (everything >1.15.0 should work), a description on how to do that can be found here. The following sections assume you have already set up a working nginx that listens for SSL connections on port 443 and have a working certificate for your domain and at least one subdomain (I will use my own server ‘atlantishq.de’ and the subdomain ‘vpn.atlantishq.de’ as example here). Also I will not go in detail on how to set up a VPN server in general and only focus on the non standard part of the openvpn configuration.

So before we start your nginx-configuration should look something like this:

http {

    ssl_certificate         /path/to/cert;
    ssl_certificate_key     /path/to/key;

    server{
        server_name atlantishq.de;
        listen [::]:443 ipv6only=off;
    }
}

Deployment via SNI multiplexing in nginx 

nginx configuration 

Essentially we now want to introduce the subdomain vpn.atlantishq.de which we connect to on port 443. First we need a stream section outside of the http section (by the way: this means we have to write the ssl-certificate paths again, if they were defined in the http section) Until stated otherwise, everything that follows takes place in the stream section.

stream {
    ssl_certificate         /path/to/cert;
    ssl_certificate_key     /path/to/key;
}

We need a mapping for subdomains, this can be achieved by using the map construct, which maps SNIs or the keyword default to certain upstreams:

map $ssl_preread_server_name $name {
    default https;
    vpn.atlantishq.de vpn;
}

Since we reference those upstreams we also have to create them. They represent our outgoing multiplexed connections which we will later proxy to the respective backends. Since they are system-internal, we can and should use unix-sockets here. Nginx will automatically generate them for us.

upstream https {
    server unix:/path/to/location/nginx/can/write/to_https ssl;
}
upstream vpn {
    server unix:/path/to/location/nginx/can/write/to_vpn ssl;
}

Now we need the two v-servers that ‘remove’ the TLS-tunnel layer from the connection and proxy it to the respective backend:

server {
    listen unix:/path/to/location/nginx/can/write/to_https
    proxy_pass 127.0.0.1:VPNPORT              # openvpn doesn't support unix-sockets
}
server{
    listen unix:/path/to/location/nginx/can/write/to_https
    proxy_pass 127.0.0.1:INTERNAL-HTTPPORT    # could also use a unix-socket here
}

Still within in the stream block, we need a server that listens for incoming connections:

server {
    listen [::]:443 ipv6only=off;
    proxy_protocol on;
    proxy_pass $name;
}

As you may notice this listen directive now conflicts with the listen directive in the http block described in the ‘Requirements’ section, and indeed we have to change the listen directive in the http block. For normal HTTPS connections the TLS-Layer is (like for the VPN traffic) already removed in the proxying servers, listening on the unix-sockets. Therefore we change the listen directive to:

http {
    ...
    server{
        ...
        listen 127.0.0.1:INTERNAL-HTTPPORT      #or whatever you used
        ...
    }
}

You have to do this for all servers previously listening on port 443. As a sidenote, if you want to enable logging in a stream block, you have to define a log-format like this:

log_format sni_multiplexer '$remote_addr [$time_local] '
        'with SNI name "$ssl_preread_server_name" '
        'proxying to "$name" '
        '$protocol $status $bytes_sent $bytes_received '
        '$session_time';         

# after that you can do
access_log /var/log/nginx/tls.log sni_multiplexer;

For me on Debian 9, subsequent starts would fail, because nginx didn’t clear the socket files on exit. You can fix this behaviour by editing the systemd-unit file with systemctl edit nginx and change --retry QUIT/5 in ExecStop to --retry QUIT/5 by writing the flowing:

[Service]
# unset exec stop
ExecStop=
# set to new value
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry TERM/5 --pidfile /run/nginx.pid

stunnel configuration 

Stunnel configuration (on the client) is relatively easy and self explaining:

[randomname]
client      = yes
accept      = 127.0.0.1:LOCAL_PORT
connect     = vpn.atlantishq.de:443
sni         = vpn.atlantishq.de
verifyChain = yes
CAPath      = /etc/ssl/certs/ 
checkHost   = vpn.atlantishq.de

So we connect to vpn.atlantishq.de:443 (with the same SNI), verify the certificate-chain, check the host certificate and expose this connection on the localhost interface on port LOCAL_PORT. The CAPath must point to the directory your trusted certificates are stored in, for Debian that is the above path.

openvpn configuration 

The only thing to note about OpenVPN is that you have to use TCP, other than that you can user your normal VPN configuration, no matter if shared secret or full scale CA with certs. There are numerous tutorials about setting up a basic VPN server, for example this one. The relevant lines for our configuration are:

serverside

# only listen on localhost
local 127.0.0.1

# and set the port
port VPN_PORT_YOU_USED_IN_NGINX

# use tls
tls-server

# the above necessitates
mode server

clientside

# use tcp
proto tcp

remote 127.0.0.1 SSTUNNEL_PORT

Deployment via client certificate multiplexing 

Now, as I said above, an active attacker could notice that I always connect to a specific subdomain, from which - with a normal web browser - he would get a seemingly empty response. As he can see that the packages, that I am receiving (encrypted of course), aren’t empty, he will likely figure out the nature of the service listening on my subdomain eventually. The solution to this is not to multiplex the connection by SNI, but by client TLS client certificate. For that you will have to create a PKI (public key infrastructure). You can find explanations and a comprehensive tutorial over at ArchWiki (also don’t let SNI bite you). Be aware at this point, that it is of course possible to stack both approaches, but I will now reuse map-/socket names and ports which breaks your nginx config unless you change them or use just one of the approaches. Also consider creating something like a stream-submodules-available and stream-submodules-enabled directory structure while using include stream-submodules-enabled/* in your main configuration to keep track of everything. We won’t have to change anything for OpenVPN, it will still connect to stunnel and nginx locally repectively and won’t even notice something changed.

nginx configuration 

Upstream stays the same, if you use the log format from above for debugging, you should probably change the SNI-line to something like:

'with cert status "$ssl_client_verify"'
'proxying to "$correct_NEW_map_name" '  <-- carefull

In the map-structure, we now have to map $ssl_client_verify instead of $ssl_preread. The former has the format "SUCCESS" and FAILED:REASON. Since we don’t really care why the certificate verification failed (if it failed), we can just match on SUCCESS in our map, and otherwise default to http.

map $ssl_client_verify $name {
    "SUCCESS"   vpn;
    default     http;
}

We need to change the v-server to now resolve/remove the TLS layer so we can access the client certificate (which at this point is protected by TLS), we don’t need ssl_preread anymore and we need to add ssl_verify_client optional to allow for client authentication (and population of the variable by the same name).

server {
    listen [::]:443 ipv6only=off ssl;

    ssl_verify_client optional;

    proxy_protocol on;
    proxy_pass $name;
}

stunnel configuration 

Just remove the SNI and add the client certificate (accepts various encodings including p12 and PEM):

[randomname]
client      = yes
accept      = 127.0.0.1:LOCAL_PORT
connect     = atlantishq.de:443
sni         = atlantishq.de
verifyChain = yes
CAPath      = /etc/ssl/certs/ 
checkHost   = atlantishq.de
cert        = /etc/stunnel/clientcert{.p12|.pem|...}

Getting HTTP2 to work/protocol multiplexing 

Using the certificate multiplexing solution, you cannot enable the HTTP2 protocol in nginx, because nginx only allows for the http2 directive to be a) in a subblock of the httpand b) the http2 directive must be in the same v-server as the ssl directive. If you want to use HTTP2 you have to multiplex the connection based on protocol (or SNI) first, and the multiplex the connections that weren’t HTTP2 (which would include the VPN connection) second. Here is a small code excerpt to give you an Idea on how to do that:

stream {
  ...
  map $ssl_preread_alpn_protocols $protocol_stream {
       default   127.0.0.1:CERT_MAP_SERVER_PORT;
       ~\bh2\b    127.0.0.1:HTTP_VSERVER_PORT;
  }

  map $ssl_verify_client $protocol_stream {
     "SUCCESS"   127.0.0.1:VPN_PORT;
     default     127.0.0.1:HTTP_VSERVER_PORT_NOSSL;
  }

  # multiplex protocol #  
  server {
        listen 443;
        ssl_preread on;
        ...
        proxy_protocol on;
        proxy_pass $protocol_stream;
  }

  # listen internall and multiplex certificate #
  server {
        # remove TLS
        listen 127.0.0.1:CERT_MAP_SERVER_PORT ssl;
        ...
        ssl_verify_client optional;

        proxy_protocol on;
        proxy_pass $name;
  }
  ...
}

http {
  ...
  server {
      listen 127.0.0.1:HTTP_VSERVER_PORT proxy_protocol ssl http2;
      listen 127.0.0.1:HTTP_VSERVER_PORT_NOSSL;
      ...
      # do stuff
      ...
  }
}

Final thoughts 

If I got the time, I would one day love to just spend a few hours in Wireshark and look at the packages to see if there are any noticeable differences - if there are any. I feel like the most suspicious thing at this point is the fact that you only use one website. It would be great to have tool that just surfs random websites for you (and not uses the VPN), but after that I feel like detection would be extremely difficult.


Attribution/Links 


Feel free to send me a mail to share your thoughts!