Categories
HAproxy Linux Network VPN Wireguard

Wireguard over TCP: proxyguard (1.x) and haproxy

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.

Wireguard (plain)
Wireguard (plain)

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.

Wireguard over TCP
Wireguard over TCP

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.

Leave a Reply

Your email address will not be published. Required fields are marked *