As shown in my last post using wireguard over TCP (with a little help of proxyguard
) isn’t that hard (once you understood how things work).
However most VPN solutions come with certain limitations: They often use their specific ports and protocols and therefore can be filtered by firewalls (either by choice or even sometime by accident).
Almost all firewall however are configured to let http(s) traffic pass. So in this post we’ll use haproxy
to allow VPN connections using the corresponding default ports 80/443
.
Of course the simplest way to do that is to change the port of your proxyguard-server
to port 80
. If that’s fine with you just stop reading, make yourself comfortable and enjoy the rest of the day 😉
But maybe your setup looks a little more like mine: I am already running haproxy
on my internet facing machine to direct different http(s) services to their corresponding internal hosts. I also use haproxy
to do a default redirect from (almost) all traffic to http/80
to https/443
.
So on my machine this port is already in use and the way to go is to make haproxy
decide which traffic needs to be forwarded to an internal web service or to my proxyguard-server
.
Step 1: Simple redirect – no encryption
We’ll just start slow and leave the https encryption aside for now. So all I wand to achieve is: Allow access to port 80
and make haproxy
decide (based on the hostname addressed) whether to redirect to https or to proxyguard-server
.

First of all we need a haproxy
backend (here “bk_proxyguard_wg0
“) for our proxyguard-server
:
server # cat /etc/haproxy/conf/proxyguard-wg0.cfg
backend bk_proxyguard_wg0
timeout connect 5s
timeout server 24h
option httpchk
#http-check send meth GET uri /proxyguard/vpn.mydomain.de ver HTTP/1.1 hdr Host vpn.mydomain.de
#http-check expect status 426
http-check send meth GET uri / ver HTTP/1.1 hdr Connection Upgrade hdr Upgrade "UoTLV/1" hdr Host vpn.mydomain.de
http-check expect status 101
http-reuse never
option http-keep-alive
timeout http-keep-alive 10s
server proxyguard_wg0_ws 127.0.0.1:51820 check
This backends points to our proxyguard-server
address. In our previous setup we needed to expose the proxyguard-server
port to the external world in order to allow clients to access that port. As soon as our desired setup works we’ll be able to change proxyguard-server
to only listen on localhost/127.0.0.1
.
The above config already uses this internal connection.
As a cleanup step we can change our wireguard
config to also only listen on localhost:
server # cat /etc/default/proxyguard-server-wg0
# Listen to localhost only, outside connection and SSL termination is handled by haproxy
LISTEN=127.0.0.1:51820
# UDP Destination
# OLD: TO=198.51.100.10:51820
TO=127.0.0.1:51820
server # systemctl restart proxyguard-server
Now we need to connect our frontend to this backend. We’ll do this by adding some rules/acls. Requests addressing vpn.mydomain.de
will use the backend defined above. However as I mentioned in the introduction I also got a default redirect from http
to https
, so we’ll have to add an exception there (“unless <...> proxyguard_http_vpn
“) otherwise the upgrade would be attempted anyway:
server # cat /etc/haproxy/conf/http.cfg
<...>
frontend ft_http_80
mode http
bind :80 name http_80_v4
<...>
# Wireguard over TCP:
acl proxyguard_http_vpn hdr(host) -i vpn.mydomain.de
use_backend bk_proxyguard_wg0 if proxyguard_http_vpn
<...>
# Redirect to https
http-request redirect scheme https unless { ssl_fc } or proxyguard_http_vpn
After that just reload haproxy
to apply the changes:
server # systemctl reload haproxy
These changes do however not affect your clients until we change their configuration to use port 80
(instead of 51820
):
client # cat /etc/default/proxyguard-client
LISTEN=127.0.0.1:51820
# Mind the missing port number: http means :80
TO=http://vpn.mydomain.de/proxyguard/
PEER_IPS=198.51.100.10
Restart the client and if everything went according to plan the VPN tunnel should be up and running again:
client # systemctl restart proxyguard-client
Step 2: Adding https encryption using haproxy
In order to uses https we require a SSL certificate. I’m using letsencrypt for that purpose and that’s where the file locations mentioned come from. The certificate retrieval is well documented (and left out here for that reason).
Basically what we’re going to implement is this: tunneling wireguard’s UDP traffic to TCP (using proxyguard
), then we’re using haproxy
to tunnel HTTP traffic and in the end adding another haproxy
tunnel for HTTPS traffic.

In order to allow https we need to add an extra frontend to our haproxy
config:
server # cat /etc/haproxy/conf/http.cfg
<...>
frontend ft_https_vpn_44301
mode http
# the certificate file needs to contain the key and was created like:
# cat /etc/letsencrypt/live/vpn.mydomain.de/privkey.pem /etc/letsencrypt/live/vpn.mydomain.de/fullchain.pem > /etc/haproxy/conf/vpn.mydomain.de.pem
bind 127.0.0.1:44301 ssl crt /etc/haproxy/certs/vpn.mydomain.de.pem
# Websocket upgrade:
# https://www.haproxy.com/blog/websockets-load-balancing-with-haproxy
maxconn 60000
default_backend bk_proxyguard_wg0
<... here follows the config from the top ...>
So we added a new http haproxy
frontend listening on localhost port 44301
using our SSL certificate. This is our SSL wrapper for the existing http haproxy
backend pointing to our proxyguard-server
.
Unfortunately we cannot just forward one https connections to another one as we want to use https for multiple hostnames on the same IP address.
So we need to define some extra TCP backend to get there:
server # cat /etc/haproxy/conf/http.cfg
<...>
backend bk_tcp2https_localhost_44301
mode tcp
<...>
option ssl-hello-chk
server https_vpn_44301 127.0.0.1:44301 check
<...>
And in the final step, we need some acl to decide which traffic should go where (as we did in step 1 for unencrypted traffic):
server # cat /etc/haproxy/conf/http.cfg
<...>
frontend ft_tcp_443
mode tcp
bind :443 name tcp_443_v4
<...>
use_backend bk_tcp2https_localhost_44301 if { req_ssl_sni -i vpn.mydomain.de
<...>
use_backend bk_tcp2https_srv_443 if { req_ssl_sni -i www.mydomain.de }
So I defined a TCP (not http!) frontend listening on the https port 443.
Haproxy
is able to check the SNI header in order to get the target hostname. Depending on that hostname different backends are used: vpn uses our proxyguard backend, www points to our webserver.