Yannik Schmidt
Jul 3, 2020 · 10 min read

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?

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 (and also kinda of illegal in my country), 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.

Legal and moral implications

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 and read carefully through it’s recommendations about browsing practices. 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 the 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, 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.

Formulating design goals for our stealth VPN

  1. must use port 443 or 80 for 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.

You will need nginx > 1.15.0 on your server, stunnel on your client, and, obviously, openvpn on both. 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 '' and the subdomain '' 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:

nginx configuration

Essentially we now want to introduce the subdomain 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.

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:

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

IMPORTANT EDIT: If you are using the latest nginx version as of October 2020, the ssl directive must be behind the listen directive, in the server block, NOT behind the server directive, in the upstream block, thanks to TikTak for pointing this out.

Now we need the two virtual servers (without ‘ssl’ directive, since we terminated TLS in the upstream-directive) and proxy the connection to the correct backend:

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

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:

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 should to define a custom log-format:

For me on Debian 9 (but this bug exists on multiple distributions), subsequent starts would fail, because nginx didn’t clear the unix-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:

stunnel configuration

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

So we connect to (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 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:



Now, as I said above, an active attacker could notice that we are always connecting to a specific subdomain, from which, with a normal web browser, he would get a seemingly empty response. As the attacker can see that the packages, which I am receiving (encrypted of course), aren’t empty, he will likely figure out the nature of the service listening on my subdomain eventually.

A possible solution to this is not to multiplex the connection by SNI, but by 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). It is of course possible to stack both approaches, but I will now reuse the previous map-/socket names and ports which will break 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:

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.

We need to change the steam-section virtual server to now resolve/remove the TLS layer so we can access the client certificate, which unlike SNI is protected by TLS. This means 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.

stunnel configuration

Just remove the “vpn.”-prefix/subdomain and add the client certificate (accepts various encodings including p12 and PEM):

Workaround for HTTP2

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. This is not an inherent problem but a bug/missing feature in nginx. 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:

Feel free to share your thoughts!

Anti Clickbait Coalition

Read about the things you want to read about, not the things you got baited into.

Anti Clickbait Coalition

The Anti Clickbait Coalition features many topics, but has strict requirements for story-titles and content, so readers can focus on the articles they really want to read.

Yannik Schmidt

Written by

Python programmer at heart, web developer on my bad days.

Anti Clickbait Coalition

The Anti Clickbait Coalition features many topics, but has strict requirements for story-titles and content, so readers can focus on the articles they really want to read.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store