WireGuard VPN setup on v-Linux (OpenVZ)

This post will explain you in details how to setup your own v-Linux server on OpenVZ platform as VPN server with WireGuard VPN.

There are many tutorials about setting up your own WireGuard VPN, but only few cover aspect of installing it on the Linux VM which runs on top of OpenVZ (Virtuozzo hypervisor). And also this tutorial will cover following aspects:

  • Installation of WireGuard VPN (wireguard-go version).
  • Configuration which supports two different concentrators on the same server:
    • One for your private use another one for other users, e.g. your friends, colleagues, etc.
    • Or it can be two (or more) groups of users in your organization.
  • IPv4 and IPv6 configuration covering:
    • Forwarding and NAT
    • Isolation of clients: blocking of inter-group (communication between groups) and intra-group (communication between peers in the group) traffic.
  • DNS server configuration (Unbound).
  • Clients configuration.

I am using Ubuntu 18.04 LTS. I suppose that my instructions are suitable for most Debian family systems.

Installation of WireGuard VPN on v-Linux

Firs step is to check what hypervisor uses your cloud provider. You shall run the following command in the terminal:

1
systemd-detect-virt

If you do not see openvz, then most probably standard installation method shall be OK for you. If, like me, you see openvz ,then go to Method 2.

Method 1: Installation of WireGuard VPN as kernel module

Standard installation of WireGuard VPN is described on the WireGuard VPN web-site.

Method 2: Installation of wireguard-go

I am using Strato.de for my virtual Linux server. Strato.de is using Virtuozzo as its cloud platform. The problem with Virtuozzo is that it is based on OpenVZ hypervisor and user cannot install additional Linux kernel modules in its VM. Strictly speaking, you can install some modules, but neither generic, nor kvm modules pass to my VM.

Therefore standard way of installation of WireGuard VPN is not possible and I had to use wireguard-go instead. Wireguard-go is written by the same team which writes WireGuard, but instead of Linux kernel space, this version of WireGuard runs in the user space.

To install wireguard-go I use following guide - WireGuard on OpenVZ/LXC

Update of the wireguard-go later

When you need to update wireguard-go, you shall stop running wireguard service before copying new version of wireguard-go to the /usr/local/bin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cd ~/source/wireguard-go
make
# you shall stop all your wireguard processes first.
# I have two and I stop both of them.
sudo systemctl stop wg-quick@wg0
sudo systemctl stop wg-quick@wg1
sudo cp wireguard-go /usr/local/bin
# start wireguard processes again.
sudo systemctl start wg-quick@wg0
sudo systemctl start wg-quick@wg1

Configuration of server

Here I will explain how to configure WireGuard VPN server and clients for two groups of clients which/whom you want completely isolate from each other.

Enable IP forwarding

In the /etc/sysctl.conf file enable IPv4 and IPv6 forwarding.

1
2
3
4
5
6
7
# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1

# Uncomment the next line to enable packet forwarding for IPv6
#  Enabling this option disables Stateless Address Autoconfiguration
#  based on Router Advertisements for this host
net.ipv6.conf.all.forwarding=1

After update of the file, run the following command to apply changes to the system:

1
sysctl -p

Now you are good to go to the next steps and start WireGuard server configuration.

Configuration of WireGuard server

Generate server keys

At first we have to generate server keys. Because we are going to have two different listening interfaces, it is better to use different server keys.

Using root privileges:

1
2
3
4
5
6
7
8
mkdir /etc/wireguard/server-group-1
mkdir /etc/wireguard/server-group-2
cd /etc/wireguard
umask 077
wg genkey | tee server1_private_key | wg pubkey > server1_public_key
mv server1_* server-group-1/
wg genkey | tee server2_private_key | wg pubkey > server2_public_key
mv server2_* server-group-2/

IP addressing

Before we go to the configuration, I want to explain my IP addressing. IPv4 and IPv6 address pools are private IPv41 and Unique Local IPv62 respectively.

Client Group IPv4 addresses pool IPv6 addresses pool
Group 1 10.255.254.0/24 fd90:abcd:6789::/64
Group 2 10.255.0.0/24 fd98:cdef:89ab::/64

I know that /64 mask for IPv6 gives much larger space of addresses as /24 mask for IPv4 (2^64 vs 255), but I do this definition for simplicity of configuration. Realistictly saying, I am going to support maybe maximum 10 clients per group anyway, therefore this difference does not matter. But if you want to be more exact, then use /120 mask for IPv6 subnets when you use /24 IPv4 subnets.

In order to set up server for two groups of users, I am using two independent WireGuard VPN configuration files:

  • wg0.conf for Group 1
  • wg1.conf for Group 2

I place them in the /etc/wireguard folder.

My wg0.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# wg0.conf
[Interface]
Address = 10.255.254.1/32,fd90:abcd:6789::1/128
SaveConfig = false
# PostUp option installs necessary iptables rules 
#   for NAT and NAT66 after services is up.
PostUp = iptables -t nat -A POSTROUTING -o venet0 -s 10.255.254.0/24 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o venet0 -s fd90:abcd:6789::/64 -j MASQUERADE
# PostDown option removes iptables rules 
#   for NAT and NAT66 when service is down.
PostDown = iptables -t nat -D POSTROUTING -o venet0 -s 10.255.254.0/24 -j MASQUERADE; ip6tables -t nat -D POSTROUTING -o venet0 -s fd90:abcd:6789::/64 -j MASQUERADE
ListenPort = 51820
PrivateKey = <Server 1 private key>

# Client 1 - Joe Doe
[Peer]
PublicKey = <Client 1 public key>
AllowedIPs = 10.255.254.2/32,fd90:abcd:6789::2/128

# Client 2 - John Smith
[Peer]
PublicKey = <Client 2 public key>
AllowedIPs = 10.255.254.3/32,fd90:abcd:6789::3/128

My wg1.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# wg1.conf
[Interface]
Address = 10.255.0.1/32,fd98:cdef:89ab::1/128
SaveConfig = false
# PostUp option installs necessary iptables rules 
#   for NAT and NAT66 after services is up.
PostUp = iptables -t nat -A POSTROUTING -o <ext_int> -s 10.255.0.0/24 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o <ext_int> -s fd98:cdef:89ab::/64 -j MASQUERADE
# PostDown option removes iptables rules 
#   for NAT and NAT66 when service is down.
PostDown = iptables -t nat -D POSTROUTING -o <ext_int> -s 10.255.0.0/24 -j MASQUERADE; ip6tables -t nat -D POSTROUTING -o <ext_int> -s fd98:cdef:89ab::/64 -j MASQUERADE
ListenPort = 51821
PrivateKey = <Server 2 private key>

# Client 1 - Koos van der Merwe
[Peer]
PublicKey = <Client 1 public key>
AllowedIPs = 10.255.0.2/32,fd98:cdef:89ab::2/128

# Client 2 - Hugo Jedermann
[Peer]
PublicKey = <Client 2 public key>
AllowedIPs = 10.255.0.3/32,fd98:cdef:89ab::3/128

Replace <ext_int> with the name of the external interface on your Linux VM. On my Linux VM this name is venet0.

DNS server configuration

As DNS server on my Linux VM I am using Unbound3.

Unbound is included in the standard repo of Ubuntu, and is simply installed:

1
sudo apt-get install unbound unbound-host

DNS Server on VPN is not the authoritative DNS. Its role is to recursively forward client DNS requests to the upstream Root DNS servers. For this purpose we have to download list of Root DNS Servers on our server:

1
2
curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.cache
chown -R unbound:unbound /var/lib/unbound

In the /etc/unbound/unbound.conf.d/ folder I created my own configuration file named unbound_srv.conf with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
server:
  # 4 threads because I have 4 x vCPUs vLinux server
  num-threads: 4
  verbosity: 1
  root-hints: "/var/lib/unbound/root.hints"

  # Server listens only on Loopback IPv4 and IPv6
  #  and on WireGuard VPN private interfaces
  #  to the clients (inside tunnels).
  interface: 127.0.0.1
  interface: 10.255.254.1
  interface: 10.255.0.1
  interface: ::1
  interface: fd90:abcd:6789::1
  interface: fd98:cdef:89ab::1
  max-udp-size: 3072

  # Blocking access from any host
  access-control: 0.0.0.0/0        refuse
  # Allow access to loopback (for system)
  access-control: 127.0.0.1        allow
  access-control: ::1        allow
  # Allow access from VPN client subnets
  access-control: 10.255.254.0/24   allow
  access-control: 10.255.0.0/24   allow
  access-control: fd90:abcd:6789::/64   allow
  access-control: fd98:cdef:89ab::/64   allow

  private-address: 10.255.254.0/24
  private-address: 10.255.0.0/24
  private-address: fd90:abcd:6789::/64
  private-address: fd98:cdef:89ab::/64

  hide-identity: yes
  hide-version: yes

  harden-glue: yes
  harden-dnssec-stripped: yes
  harden-referral-path: yes

  unwanted-reply-threshold: 10000000

  val-log-level: 1
  cache-min-ttl: 60
  # I use short TTL for the cached records
  cache-max-ttl: 300
  prefetch: yes
  prefetch-key: yes

Configuration is ready and you can start DNS server:

1
2
sudo systemctl enable unbound
sudo systemctl start unbound

You shall configure and start WireGuard VPN server on all necessary interfaces before you start Unbound, otherwise Unbound will not start and give you an error, because interfaces on which it shall listen on are not ready yet.

Configuration of iptables

I am not going to give all the theory about iptables, but you shall remember, that rules order matters!.

Initial configuration

Initially I have to block all unnecessary incoming traffic. And for this purpose I use following rules set.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -i wg0 -j ACCEPT
sudo iptables -A INPUT -i wg1 -j ACCEPT
sudo iptables -A INPUT -p icmp -m icmp --icmp-type 8 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT
sudo iptables -A INPUT -p udp -m udp --dport 51821 -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A INPUT -j DROP

sudo ip6tables -A INPUT -i lo -j ACCEPT
sudo ip6tables -A INPUT -i wg0 -j ACCEPT
sudo ip6tables -A INPUT -i wg1 -j ACCEPT
sudo ip6tables -A INPUT -p ipv6-icmp -j ACCEPT
sudo ip6tables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
sudo ip6tables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT
sudo ip6tables -A INPUT -p udp -m udp --dport 51821 -j ACCEPT
sudo ip6tables -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo ip6tables -A INPUT -j DROP

Groups and users isolation

In order to isolate peers from traffic exchange, I use two different approaches simultaneously:

  • Blocking traffic using INPUT filters.
  • Blocking traffic using FORWARDING restrictions.

I use both of them because somehow one works for IPv4, but does not work for IPv6 and vice versa. Maybe it is a bug in iptables implementation or maybe it is normal behavior. I cannot figure this out, but implementation of them simultaneously gives results.

Isolation using FORWARDING rules is installed like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# For IPv4
sudo iptables -A FORWARD -i wg1 -o wg1 -j REJECT --reject-with icmp-admin-prohibited
sudo iptables -A FORWARD -i wg0 -o wg0 -j REJECT --reject-with icmp-admin-prohibited
sudo iptables -A FORWARD -i wg1 -o wg0 -j REJECT --reject-with icmp-admin-prohibited
sudo iptables -A FORWARD -i wg0 -o wg1 -j REJECT --reject-with icmp-admin-prohibited
# For IPv6
sudo ip6tables -A FORWARD -i wg1 -o wg1 -j REJECT --reject-with icmp6-adm-prohibited
sudo ip6tables -A FORWARD -i wg0 -o wg0 -j REJECT --reject-with icmp6-adm-prohibited
sudo ip6tables -A FORWARD -i wg0 -o wg1 -j REJECT --reject-with icmp6-adm-prohibited
sudo ip6tables -A FORWARD -i wg1 -o wg0 -j REJECT --reject-with icmp6-adm-prohibited

And then I also use isolating filters, which I apply on INPUT chain. For this I use my own rules chain called “wg-rules”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
## For IPv4
sudo iptables -N wg-rules
# Isolation of group subnets from each-other
sudo iptables -A wg-rules -s 10.255.0.0/24 -d 10.255.254.0/24 -j DROP
sudo iptables -A wg-rules -s 10.255.254.0/24 -d 10.255.0.0/24 -j DROP
# Inserting this rule as the rule number 2.
#  Rule 1 in my configuration allows traffic on loopback.
sudo iptables -I INPUT 2 -j wg-rules

## For IPv6
sudo ip6tables -N wg-rules
# Isolation of group subnets from each-other
sudo ip6tables -A wg-rules -s fd90:abcd:6789::/64 -d fd98:cdef:89ab::/64 -j DROP
sudo ip6tables -A wg-rules -s fd98:cdef:89ab::/64 -d fd90:abcd:6789::/64 -j DROP
# Inserting this rule as the rule number 2.
#  Rule 1 in my configuration allows traffic on loopback.
sudo ip6tables -I INPUT 2 -j wg-rules

Listening on multiple ports

WireGuard VPN cannot listen on multiple ports. If you want your clients being able to connect on different ports, you shall use iptables for this. E.g. server 1 listens on the UDP port 51820, and you want to open port 1194 (use OpenVPN port for WireGuard) in addition. For this case you shall add following configuration to iptables.

First for IPv4:

1
2
3
4
# <ext_int> is your external port
sudo iptables -t nat -A PREROUTING -i <ext_int> -p udp -m udp --dport 1194 -j REDIRECT --to-ports 51820
# <RULE NUMBER> shall be less than the last "-A INPUT -j DROP" rule
sudo iptables -I INPUT <RULE NUMBER> -p udp -m udp --dport 1194 -j ACCEPT

And then for IPv6:

1
2
3
4
# <ext_int> is your external port
sudo ip6tables -t nat -A PREROUTING -i <ext_int> -p udp -m udp --dport 1194 -j REDIRECT --to-ports 51820
# <RULE NUMBER> shall be less than the last "-A INPUT -j DROP" rule
sudo ip6tables -I INPUT <RULE NUMBER> -p udp -m udp --dport 1194 -j ACCEPT

As a result, your WireGuard VPN Server 1 will accept client connections on ports 1194 and 51820.

Summary example

My own iptables configuration looks like this (for IPv4):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N wg-rules

## INPUT rules
# Allow traffic on loopback
-A INPUT -i lo -j ACCEPT
# WireGuard rules chain
-A INPUT -j wg-rules
-A INPUT -i wg0 -j ACCEPT
-A INPUT -i wg1 -j ACCEPT
-A INPUT -p icmp -m icmp --icmp-type 8 -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
# Allow SSH
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -p udp -m udp --dport 1194 -j ACCEPT
-A INPUT -p udp -m udp --dport 1195 -j ACCEPT
-A INPUT -p udp -m udp --dport 51820 -j ACCEPT
-A INPUT -p udp -m udp --dport 51821 -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -j DROP

## Forwarding rules
-A FORWARD -i wg1 -o wg1 -j REJECT --reject-with icmp-admin-prohibited
-A FORWARD -i wg0 -o wg0 -j REJECT --reject-with icmp-admin-prohibited
-A FORWARD -i wg1 -o wg0 -j REJECT --reject-with icmp-admin-prohibited
-A FORWARD -i wg0 -o wg1 -j REJECT --reject-with icmp-admin-prohibited

## WireGuard rules chain definition
# Isolation of group subnets from each-other
-A wg-rules -s 10.255.0.0/24 -d 10.255.254.0/24 -j DROP
-A wg-rules -s 10.255.254.0/24 -d 10.255.0.0/24 -j DROP

## NAT rules
sudo iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
# Port redirectoin
-A PREROUTING -i venet0 -p udp -m udp --dport 1194 -j REDIRECT --to-ports 51820
-A PREROUTING -i venet0 -p udp -m udp --dport 1195 -j REDIRECT --to-ports 51821

# Dynamic NAT rules instaled by the wg-quick service
-A POSTROUTING -s 10.255.254.0/24 -o venet0 -j MASQUERADE
-A POSTROUTING -s 10.255.0.0/24 -o venet0 -j MASQUERADE

For IPv6 rules configuration is similar.

Persistence of iptables rules

Iptables rules will be cleaned with the next reboot. Therefore, in order to save them and recover after system reboot, I install iptables-persistent tool:

1
sudo apt-get install iptables-persistent

And when I modify iptables configuration, I save rules and they will be applied again after boot automatically.

1
2
sudo bash -c 'iptables-save > /etc/iptables/rules.v4'
sudo bash -c 'ip6tables-save > /etc/iptables/rules.v6'

Clients

Ideally, each client shall generate its own private key locally and then share with me only public key. In reality I create keys and configuration file for each client locally on the server. To clients on Android and iOS I am sending QR code which is generated from the client configuration file. For the PC clients (Linux, Windows) I have to use other methods like USB, encrypted e-mail, etc.

I prefer to store clients keys and configurations in separate subfolders. I use subfolder per each VPN interface running.

Using root privileges:

1
2
mkdir /etc/wireguard/group-1-clients
mkdir /etc/wireguard/group-2-clients

Client keys are generated using the same command, which I use to generate server keys. E.g. for the client “Joe Doe” from Group 1:

1
2
3
4
cd /etc/wireguard/group-1-clients
wg genkey | tee Joe_Doe_private_key | wg pubkey > Joe_Doe_public_key
touch Joe_Doe.conf
chmod 600 Joe_Doe*

Joe Doe’s configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Joe Doe's configuration file
[Interface]
Address = 10.255.254.2/32,fd90:abcd:6789::2/128
PrivateKey = <Joe Doe private key>
# DNS on server is used
DNS = 10.255.254.1,fd90:abcd:6789::1

[Peer]
PublicKey = <Server 1 public key>
Endpoint = <Your Linux VM public IP>:51820
# All traffic is sent via VPN server.
AllowedIPs = 0.0.0.0/0,::/0

Android, iOS

On Android and iOS devices you can use default WireGuard client:

In order to configure mobile clients, it is much more convenient to send client configurations as QR code pictures. I installed qrencode tool:

1
sudo apt-get install qrencode

For the example above (Joe Doe config) you can generate QR code using following command:

1
qrencode -t ansiutf8 < Joe_Doe.conf

In the terminal you will see the QR code like this.

Linux

Linux clients can install WireGuard using instructions from WireGuard VPN web-site. On Linux client PC configuration file is stored in the same /etc/wireguard folder.

You can start VPN connection with the following command:

1
sudo wg-quick up wg0

And you can check status of the connection with the command:

1
sudo wg show

Windows

Official WireGuard client for Windows is not very stable, and it is better to use TunSafe instead. Use of this client is quite straightforward, You just import provided configuration file in the client, and you good to go.

  1. wireguard-go
  2. WireGuard on OpenVZ/LXC
  3. Wireguard VPN: Typical Setup