I’ve used Mullvad as my VPN provider for a few years. Their service is good, they provide keys for 5 devices, rely on the Wireguard protocol, and offer alternative configurations as well. Despite that, my needs have changed and I wanted to be able to have granular control over DNS queries and the ability to connect my various devices on my network through simple addresses, such as http://emby.home.server. Enter Wireguard, Pi-Hole and Unbound.
Of course, there are many tools that might achieve the same results with a lot less work, such as Tailscale. I would reach devices on the network easily, one of the nodes could be set to act as an exit node and you could also apply network-wide ad-block with Next DNS (I believe that is, at the time of writing, the solution they offer). Outside of Tailscale there are other fully open source alternatives, such as Headscale, Nebula and others. However, I chose to set up my own Wireguard network, as it gave me the opportunity to learn a lot and helped me better understand everything involved in the process.
For devices, I have the following configuration, and the below IPs will be used throughout the article. They represent a starting point, but should be sufficient for anyone to build their network according to their needs.
10.10.10.1
) - running on a VPS (Virtual Private Server) from a provider that meets my requirements (close physical proximity, high bandwidth and at least 1 Gbps transfer speeds)10.10.10.10
) - running on my home network and providing various service (media server, personal cloud, torrent client, etc.)10.10.10.11
) - running on my home network10.10.10.12
) - running on my home network, but might connect through outside networks (i.e.: coffee shops, libraries, airports, etc.)10.10.10.100
) - running on my home network, mobile data, mobile data roaming or public networks (coffee shops, libraries, airports, etc.)10.10.10.101
) - same as aboveI will use a hub and spoke network topology where the VPS will act as the in-between for all inter-device communications on the network as well as the exit node for all Internet-facing communications. For example, if my Desktop connects to the Internet it will do so by navigating to the VPS, which will then resolve the query and block any domains that are on the block list. If it wants to connect to the Home Server it will do so without the VPS, since the Home Server and Desktop are on the same local network, connected via a router. If Smartphone 1 wants to connect to the Internet, the query is resolved by the VPS, which will block any domains on the block list. If it wants to connect to the Home Server, the VPS will receive the request and route it to the IP of the Home Server. It’s important to keep in mind that the contents of the request become visible as they leave the Wireguard tunnel between Smartphone 1 and the VPS. On the VPS contents will be encrypted again and then forwarded to the Home Server.
I chose a VPS provider that is physically near my location, has minimum 1 Gbps speed (up/down), unlimited bandwidth and good performance while gaming (i.e.: specifically no packet loss). There are many options; you can check resources like LowEndBox and VPSBenchmarks to get an idea of price ranges, availability and regions. For your own needs, start by defining a set of requirements and see which of these match your needs.
My only recommendation is to initially opt for a cheaper provider, that offers hourly billing and understand the end-to-end-process before committing. Keep in mind that various providers have different configurations and settings, so setups can vary.
If you are completely new to setting up servers using a command line interface (CLI) only, I recommend checking out the Digital Ocean cloud computing series. Most providers have their own guides, so for specific steps check their respective documentation. Same goes for any chosen OS. For this guide I will be using Ubuntu 24.04. For the remainder of the article I will assume you are running an OS with access to a terminal that can send commands to a CLI that can generate SSH keys, establish SSH connections and can run a text editor that you can use to create, edit, update and delete text files. I will be using BASH/Fish with nano
as my text editor.
To start, create an SSH key on your local machine and upload it to your chosen cloud provider. This first step already greatly improves security of the VPS and simplifies connection to it as you don’t need to remember a password.
# Run inside your terminal on your client machine
# https://www.man7.org/linux/man-pages/man1/ssh-keygen.1.html
$ ssh-keygen -t ed25519
Follow the on-screen prompts to provide a name, set a passphrase (recommended) and have the private and public files created at the given path in the prompt.
Next, copy the contents of the .pub file, or the file itself to your chosen VPS provider’s server setup page. Complete the initial server setup as per your provider’s steps. Once ready, you will have a public IPv4 address to which you can connect (this is a requirement in this guide, and some providers might only offer IPv6 addresses on their cheapest VPS). A connection can now be established with:
# replace root with the username your provider created by default; could be ubuntu as well
# replace 123.123.123.123 with the IP address of your VPS
$ ssh -i .ssh/article root@123.123.123.123
You are now connected to the server, as the root
user. If you connected via another user that has sudo
access, then prepend all commands with sudo
. If I write ufw status numbered
use sudo ufw status numbered
.
First, make sure you have unattended-upgrades
installed on your VPS.
$ apt install unattended-upgrades
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
unattended-upgrades is already the newest version (2.9.1+nmu4ubuntu1).
0 upgraded, 0 newly installed, 0 to remove and 29 not upgraded.
Next, prepare the settings you want to apply. Because the application reads through different configuration files in order, I made a copy of /etc/apt/apt.conf.d/50unattended-upgrades and called it /etc/apt/apt.conf.d/51unattended-upgrades. This means that even after an update, while the original file could be overwritten, my settings will persist. The main settings I changed in my copy are found below. Read through the different options and adjust to your preferences.
# Make a copy and give a new name
$ cp /etc/apt/apt.conf.d/50unattended-upgrades /etc/apt/apt.conf.d/51unattended-upgrades
# Edit the file
$ nano /etc/apt/apt.conf.d/51unattended-upgrades
# File contents below are edited for brevity
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
// Extended Security Maintenance; doesn't necessarily exist for
// every release and this system may not have it installed, but if
// available, the policy for updates is such that unattended-upgrades
// should also install from here by default.
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
"${distro_id}:${distro_codename}-updates";
};
// Remove unused automatically installed kernel-related packages
// (kernel images, kernel headers and kernel version locked tools).
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// Do automatic removal of newly unused dependencies after the upgrade
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Do automatic removal of unused packages after the upgrade
// (equivalent to apt-get autoremove)
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Automatically reboot *WITHOUT CONFIRMATION* if
// the file /var/run/reboot-required is found after the upgrade
Unattended-Upgrade::Automatic-Reboot "true";
// If automatic reboot is enabled and needed, reboot at the specific
// time instead of immediately
// Default: "now" - change this to whatever works for you!
Unattended-Upgrade::Automatic-Reboot-Time "00:00";
Next, update the periodic file to provide unattended-upgrades
with the intervals at which it checks for updates and runs the clean command. Create a new file at /etc/apt/apt.conf.d/21periodic (you can name it anything else, as these are parsed in order, so 22periodic, 23…). The below are the values for the purposes of this server:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
Number 1 means the option is enabled and runs daily. Zero means the option is turned off. Any other number shows the frequency with which the step is completed (e.g.: 7 - the action is performed once a week).
Run unattended-upgrade -d
to quickly debug your current app configuration and confirm the settings you have setup so far.
Next off, I configure the system hostname. The hostname can then be used as part of a fully qualified domain name for the system (e.g.: hostname = pihole, fqdn = pihole.psyonik.com)
# you can check current hostname with
hostname
# set hostname to something else
hostnamectl set-hostname pihole
# edit hosts file to create static associations between IP addresses and hostnames/domains
# which the system prioritizes before DNS for name resolution
nano /etc/hosts
# The contents of the file might be similar, adjust them to match what I have below
# replacing <pihole> and <psyonik> with your settings
127.0.0.1 localhost
127.0.0.1 pihole
127.0.1.1 pihole.psyonik.com pihole
# replace the below address with your VPS actual IP address
123.123.123.113 pihole.psyonik.com pihole
I check if the hostname is setup correctly by calling ping pihole.psyonik.com
.
As mentioned previously, I connect to the server by using ssh -i .ssh/article root@123.123.123.123
. The command passes in the path to the SSH key I use, sets the username I want to connect as, and sets the IP address of the server. By default, the SSH service runs on port 22. This is not explicitly stated in the connection command, it is just assumed as being set as such. One of the first things I like to change on a new VPS is the default port to something else. On Ubuntu 24.04 you will have this option in the sshd_config file, but the actual port used will be picked up from the ssh.socket
service. This can be confusing, as the setting is still present in the sshd_config file.
The first step is to make a copy of the configuration file and then edit the following lines in the original file.
$ cp /etc/ssh/sshd_config /etc/ssh/sshd_config.1
# Edit the SSH configuration - disable root login, password auth, enable pubkey login
$ nano /etc/ssh/sshd_config
# Find and edit the below values in the file
AddressFamily inet # SSH Service will only listen to IPv4 addresses
PermitRootLogin no # disable root login
PubkeyAuthentication yes # only allow SSH key-based authentication
AuthorizedKeysFile .ssh/authorized_keys # file that contains allowed public keys
PasswordAuthentication no # do not allow password auth
PermitEmptyPasswords no # do not allow empty passwords
ChallengeResponseAuthentication no # Specifies if challenge-response auth is allowed
UsePAM no # disable authentication through PAM (Pluggable Authentication Module)
Next, I create a folder and a configuration file to change the SSH port for the ssh.socket
.
$ mkdir /etc/systemd/system/ssh.socket.d
# Create and add the values into the listen.conf file
$ nano /etc/systemd/system/ssh.socket.d/listen.conf
[Socket]
ListenStream=
ListenStream=12345
# Save and exit file
$ systemctl daemon-reload
# Restart the SSH Socket
$ systemctl restart ssh.socket
# Restart the SSH Service
$ systemctl restart ssh.service
# Check the status of the service after restart; you should see the new port number displayed
$ systemctl status ssh.service
# Output for my configuration
● ssh.service - OpenBSD Secure Shell server
Loaded: loaded (/usr/lib/systemd/system/ssh.service; disabled; preset: enabled)
Active: active (running) since Sun 1999-01-01 12:01:04 UTC; 5s ago
TriggeredBy: ● ssh.socket
Docs: man:sshd(8)
man:sshd_config(5)
Process: 12928 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS)
Main PID: 12929 (sshd)
Tasks: 1 (limit: 4543)
Memory: 1.2M (peak: 1.3M)
CPU: 36ms
CGroup: /system.slice/ssh.service
└─12929 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups"
Jan 01 12:01:04 pihole systemd[1]: Starting ssh.service - OpenBSD Secure Shell server...
Jan 01 12:01:04 pihole sshd[12929]: Server listening on :: port 12345.
Jan 01 12:01:04 pihole systemd[1]: Started ssh.service - OpenBSD Secure Shell server.
Now, in a new tab, if I try to connect with the previous command, this should not work. I would now also need to specify the port, but if I try to connect as root
this should get denied.
$ ssh -i .ssh/article -p 12345 root@123.123.123.123 # this should return an error
While keeping my session open, I will create a new user, add the user to sudo
, copy the .ssh/authorized_keys file over to the new users home directory and try to connect with that user. I then complete the remaining configuration using root
.
# Create a new user and follow on-screen prompts
$ adduser psyonik
# Add user to sudo
$ usermod -aG sudo psyonik
# Copy authorizedkeys file to the new user folder and change ownership
$ cp -r .ssh/ /home/psyonik/
$ chown psyonik:psyonik -R /home/psyonik/.ssh
# Ensure ownership is set correctly
$ ls -alh /home/psyonik/.ssh/
total 12K
drwx------ 2 psyonik psyonik 4.0K Jan 1 12:09 .
drwxr-x--- 3 psyonik psyonik 4.0K Jan 1 12:09 ..
-rw------- 1 psyonik psyonik 162 Jan 1 12:09 authorized_keys
When I connect from my local machine to the VPS using this new username, the connection should be established correctly.
$ ssh -i .ssh/article -p 12345 psyonik@123.123.123.123
This completes the setup of SSH. Next up, we setup the firewall. If you encounter issues, you can use ssh -vvv
to have verbose logging enabled during the SSH connection. This should highlight any issues such as access denied, incorrect keys or anything else of the sort.
Although the server can now do automatic updates, doesn’t rely on the default port to allow incoming connections and has root
login disabled, we still need to configure the firewall. There are various ways to do this, but I like to use ufw
.
Uncomplicated firewall is a great way to setup some basic rules and security on the server. I allow only 2 ports open on IPv4 on the server at this stage, enable logging, and deny all other incoming requests while allowing all outgoing requests.
# the SSH port should be replaced with whatever you wish to use instead
$ ufw allow from 0.0.0.0/0 to any port 12345 comment "SSH"
$ ufw allow from 0.0.0.0/0 to any port 51820 comment "Wireguard"
# block all incoming connections except those going to the SSH/Wireguard ports
$ ufw default deny incoming
# allow
$ ufw default allow outgoing
# enable logging
$ ufw logging on
# enable ufw and display the rules in a numbered list
$ ufw enable
$ ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] 12345 ALLOW IN Anywhere # SSH
[ 2] 51820 ALLOW IN Anywhere # Wireguard
Each time you edit a rule you can call ufw reload
to refresh the new rules. You can then check the status of the rules with ufw status numbered
. This also gives you an easy way to delete any unused rules.
Lastly, there is one more change that can be done so that the server will not respond to ping requests.
sudo nano /etc/ufw/before.rules
# Look for the following row and change from ACCEPT to DROP
-A ufw-before-input -p icmp --icmp-type echo-request -j DROP
This completes a basic server configuration for this machine. Other settings can be changed and if I were to do this often, I would opt for a script file as a minimum, or ideally a cloud-init script. Feel free to explore that once you feel comfortable with this setup.
Next, I’ll setup Wireguard, Pi-Hole and Unbound.
Unlike traditional VPN services, where a central server acts as a point of control and has clients, Wireguard uses the concept of peers. Peers can connect directly to each other, thus allowing for lower latency for connections and removing the single point of failure of a server. However, for my purposes, I configured one of the peers, in Wireguard parlance, to act as a server due to the hub-and-spoke network topology I employ in this setup. As such, the VPS will be referred to as the ‘server’ and all other devices as ‘clients’.
I will expand this article once I have a configuration in which clients that need to be able to connect to each other will do so directly, without a VPS in-between.
Next, to setup each client and the server, I need to create keys for all devices and create configuration files using these keys. The below steps will create public, private and pre-shared keys for all devices.
# Install or make sure Wireguard is installed
$ apt install wireguard
# Create a folder to keep client keys and one for client configurations
# This is not strictly needed, as once clients are added, the keys can be removed from the server
# Except for the preshared keys
$ mkdir /etc/wireguard/clients
$ mkdir /etc/wireguard/clientconfs
# Create VPS keys - key, pub, psk
wg genkey > vps.key # I use .key to show we're dealing with a private key
wg pubkey < vps.key > vps.pub # I use .pub to show this is a public key
wg genkey > /etc/wireguard/clients/homeserver.key
wg pubkey < /etc/wireguard/clients/homeserver.key > /etc/wireguard/clients/homeserver.pub
wg genpsk > /etc/wireguard/clients/homeserver.psk # I use .psk to show this is a preshared key
wg genkey > /etc/wireguard/clients/desktop.key
wg pubkey < /etc/wireguard/clients/desktop.key > /etc/wireguard/clients/desktop.pub
wg genpsk > /etc/wireguard/clients/desktop.psk
wg genkey > /etc/wireguard/clients/laptop.key
wg pubkey < /etc/wireguard/clients/laptop.key > /etc/wireguard/clients/laptop.pub
wg genpsk > /etc/wireguard/clients/laptop.psk
wg genkey > /etc/wireguard/clients/mobile1.key
wg pubkey < /etc/wireguard/clients/mobile1.key > /etc/wireguard/clients/mobile1.pub
wg genpsk > /etc/wireguard/clients/mobile1.psk
wg genkey > /etc/wireguard/clients/mobile2.key
wg pubkey < /etc/wireguard/clients/mobile2.key > /etc/wireguard/clients/mobile2.pub
wg genpsk > /etc/wireguard/clients/mobile2.psk
With the keys created, I prepare the VPS configuration file. Each peer that wants to join the VPN needs its own configuration file. In Wireguard, the configuration file is split into two main sections: [Interface]
and [Peer]
. Each configuration file can contain zero, one or multiple peers that I can connect to. If I have one peer in my config, I’m connecting to that one device, if I have more, it means I can directly connect to all those peers.
In this case, the server will have multiple peers while the peers will each have a single peer: the server. The term interface is used because Wireguard creates a new network interface which is named according to the name of the configuration file.
Following this, I create a new configuration file named wg0, which means that once I start Wireguard with this configuration, I will have a new network interface called wg0
. This can be renamed to anything else.
$ nano /etc/wireguard/wg0.conf
# Next I will populate the file with the below information
[Interface]
# Replace the contents with the value you got in vps.key
PrivateKey = QELYVAdCPCaVQfoyE3KrADMZBBdoNotUseAsKeyThisIsMine+eWUA=
Address = 10.10.10.1/24 # the IP address the server will have inside the wg0 network
ListenPort = 51820 # the port on which the server will listen for incoming connections
SaveConfig = true # https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html
PreUp = sysctl -w net.ipv4.ip_forward=1 # enables ipv4 forwarding on the VPS
# PreUp = sysctl -w net.ipv6.conf.all.forward=1 if you use ipv6 instead
At this stage I have the base for my Wireguard network on the server. But because I want to hide traffic from peers behind the server’s IP address I need to apply masquerading to the incoming requests from the clients, a function similar to NAT. To achieve this, we need to get the data coming in on the network interface Wireguard creates out to the network interface of the server and allow the server to alter the source IP.
The first step is finding the default network interface on the server, so I run the below command:
$ ip route list default
# Output could be something like:
# default via 123.123.123.123 dev eth0 proto dhcp src 123.123.123.123 metric 100
The important bit here is to see the device (in my sample above it is dev eth0
). This means I want to route requests that I receive out eth0
and apply masquerading, which makes it look like a request received from a smartphone originated from the server.
I will go back to the configuration and continue applying a few more settings to enable masquerading out the eth0
interface (again, this can be different on your machine) and allow traffic that comes in on the wg0
interface (named after the configuration file) to go out on eth0
.
$ nano /etc/wireguard/wg0.conf
# Next I will populate the file with the below information
[Interface]
PrivateKey = QELYVAdCPCaVQfoyE3KrADMZBBdoNotUseAsKeyThisIsMine+eWUA=
Address = 10.10.10.1/24 # the IP address the server will have inside the wg0 network
ListenPort = 51820 # the port on which the server will listen for incoming connections
SaveConfig = true # https://manpages.debian.org/unstable/wireguard-tools/wg-quick.8.en.html
PreUp = sysctl -w net.ipv4.ip_forward=1 # enables ipv4 forwarding on the VPS
# PreUp = sysctl -w net.ipv6.conf.all.forward=1 if you use ipv6 instead
PostUp= ufw route allow in on wg0 out on eth0
PostUp= iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
PreDown= ufw route delete allow in on wg0 out on eth0
PreDown= iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
This completes the configuration of the Wireguard interface. After exiting and saving the file, I enable a systemd
service that follows the name of the config file/interface and check to make sure it runs.
# Enable Wireguard service at system startup
$ systemctl enable wg-quick@wg0.service
# Start Wireguard service
$ systemctl start wg-quick@wg0.service
# Check service status
$ systemctl status wg-quick@wg0.service
● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
Loaded: loaded (/usr/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)
Active: active (exited) since Sun 2024-01-01 12:12:42 UTC; 1min 29s ago
Docs: man:wg-quick(8)
man:wg(8)
https://www.wireguard.com/
https://www.wireguard.com/quickstart/
https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
Process: 13987 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
Main PID: 13987 (code=exited, status=0/SUCCESS)
CPU: 325ms
Jan 01 12:12:41 pihole wg-quick[13997]: net.ipv4.ip_forward = 1
Jan 01 12:12:41 pihole wg-quick[13987]: [#] ip link add wg0 type wireguard
Jan 01 12:12:41 pihole wg-quick[13987]: [#] wg setconf wg0 /dev/fd/63
Jan 01 12:12:41 pihole wg-quick[13987]: [#] ip -4 address add 10.10.10.1/24 dev wg0
Jan 01 12:12:41 pihole wg-quick[13987]: [#] ip link set mtu 1420 up dev wg0
Jan 01 12:12:41 pihole wg-quick[13987]: [#] ufw route allow in on wg0 out on eth0
Jan 01 12:12:41 pihole wg-quick[14017]: Rule added
Jan 01 12:12:41 pihole wg-quick[14017]: Rule added (v6)
Jan 01 12:12:42 pihole wg-quick[13987]: [#] iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
Jan 01 12:12:42 pihole systemd[1]: Finished wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0.
# Check wireguard status directly and verify the key
$ wg
interface: wg0
public key: qUo/OLUZadoNotUseAsKeyThisIsMineokE6T3pYl0c=
private key: (hidden)
listening port: 51820
# Show network interfaces
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 1f:1f:1f:1f:1f:1f brd ff:ff:ff:ff:ff:ff
inet 123.123.123.123/32 metric 100 scope global dynamic eth0
valid_lft 60719sec preferred_lft 60719sec
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
link/none
inet 10.10.10.1/24 scope global wg0
valid_lft forever preferred_lft forever
After this, I setup the first peer - remember, I need a configuration file for this. The private key field holds the private key of the peer, the pre-shared key is the value of the pre-shared key I created for the peer, and the public key section in the file holds the public key of the VPS.
# Just naming it dwg0 as for me it will be easy to see that this is the desktop config
# To bring this interface up on my desktop I would use `sudo wg-quick up dwg0`
$ nano dwg0.conf
[Interface]
# Replace with the private key file contents for THIS particular device, in my case desktop
PrivateKey = uIwALjBCXdoNotUseAsKeyThisIsMine1Vb/kI3XGXk=
# The address I had mentioned initially in the topology overview
Address = 10.10.10.11/32
# The VPS will act as a DNS server for this device
DNS = 10.10.10.1
[Peer]
# Replace with the public key file contents for your VPS, in my case vps.pub
PublicKey = qUo/OLUdoNotUseAsKeyThisIsMineWgokE6T3pYl0c=
# Replace with the value you got in your .psk file for THIS particular device, in my case desktop
PresharedKey = pMTDdoNotUseAsKeyThisIsMineeaGilNZRO9OGy3Q=
# https://www.procustodibus.com/blog/2021/03/wireguard-allowedips-calculator/#background
AllowedIPs = 0.0.0.0/0
# Replace 123.123.123.123:51820 with the IP of your VPS and the port you used if different
Endpoint = 123.123.123.123:51820
PersistentKeepalive = 25 # https://wiki.archlinux.org/title/WireGuard#Unable_to_establish_a_persistent_connection_behind_NAT_/_firewall
At this stage, if I enable the configuration on my desktop, it will not connect to the server. First, I need to add the desktop as a peer of the VPS. I can do that by either bringing down the wg0
interface or by using the wg set
command. I use the latter, which edits the /etc/wireguard/wg0.conf file and adds a new entry under the [Peer]
section.
# The key that I provide to the command is the public key of the desktop
$ wg set wg0 peer KKdoNotUseAsKeyThisIsMineRFCyZVorUW8m7E/3S0= preshared-key /etc/wireguard/clients/desktop.psk allowed-ips 10.10.10.11
# Check the peer has been added to the interface
$ wg
interface: wg0
public key: qUo/OLUdoNotUseAsKeyThisIsMineWgokE6T3pYl0c=
private key: (hidden)
listening port: 51820
peer: KKdoNotUseAsKeyThisIsMineRFCyZVorUW8m7E/3S0=
preshared key: (hidden)
allowed ips: 10.10.10.11/32
This completes this part of the setup. To use the server to resolve DNS queries and block ads, I setup Pi-Hole and Unbound next.
I installed Pi-Hole using the automated script from their website. I won’t go into the details of piping a script to bash directly, but for those of you who wish to dissect the script or use alternative methods, you can clone the repo and run the script that way, or you can download the installer file and run it. You can also use Docker to deploy it, but I won’t cover those in this guide.
$ curl -sSL https://install.pi-hole.net | bash
You’ll be greeted by a step by step graphical install. Assuming you’re using a VPS with a dedicated IP, you can confirm that you do have a Static IP and select Continue. You’ll next be asked for the interface, at this stage select wg0
as we want Pi-Hole to run for those connected on the Wireguard interface.
For upstream DNS provider I selected Quad9 for now. Next you’ll be asked to confirm using the suggested block list, select Yes and continue the install. Select Yes for the admin interface and select Yes to install lighttpd
and the required PHP modules (adjust if you don’t wish to use these). It is up to yourself if you wish to enable query logging or not. You can turn this off later on, if you enable it now. Select the privacy level for the query log.
Once the install finishes you should see the IP on which Pi-Hole is running, the interface on which it is running and the password. Make sure to save the password.
With both the server and the client configured and Pi-Hole installed, we’re close to turning on the client. Before that, there is one thing to consider. The client configuration sets the server as the DNS resolver. However, the server currently only allows access to port 12345 (SSH) and port 51820 for Wireguard. Ubuntu has systemd-resolved
using port 53 for DNS resolution, but access to this port is currently restricted, so I’ll add one more rule to UFW on the server, to allow requests to port 53 coming from the Wireguard IP range I defined earlier.
I also want to be able to configure Pi-Hole once I bring the Wireguard interface up, so for that I need to tell UFW to allow connections to port 80 (HTTP) on the Wireguard IP range.
# Allow requests to port 53 from any ip in the 10.10.10.1 - 10.10.10.255 range
$ ufw allow from 10.10.10.0/24 to any port 53
# Allow requests to port 80 from any ip in the 10.10.10.1 - 10.10.10.255 range
$ ufw allow from 10.10.10.0/24 to any port 80
# Reload firewall to apply settings
$ ufw reload
# Check rules are showing up
$ ufw status numbered
At this point, I can copy the contents of the client config to my desktop, bring up the interface on the desktop (using wg-quick
) and navigate to http://10.10.10.1/admin and change any settings on the Pi-Hole instance running on the VPS server. But before doing that, I want to have Unbound setup on the server too, and then bring up the interface on my desktop and change the DNS resolver on Pi-Hole from Quad9 to Unbound.
Unbound is a recursive, caching DNS resolver. During the Pi-Hole setup, I had picked an upstream DNS resolver, but the issue is, from a privacy standpoint, that the upstream server (in this case Quad9) knows my queries and the queries of everyone using VPS as their DNS, because it’s not VPS resolving the query, it just forwards it to the upstream provider. More information on this, along with the Unbound configuration and setup can be found on the Pi-Hole website.
In this guide I’ll just walk you through the setup of Unbound and the configuration I use. To start off, I’ll install unbound and configure it.
$ apt install unbound
Chances are that at this point when you check the status of the Unbound service, it shows as failed. The reason is that you have already a resolver running on port 53. To get around this, I setup a new configuration for Unbound in line with the one provided in the Pi-Hole documentation, with a few tweaks to match my VPS and IP ranges.
I create the new configuration file at /etc/unbound/unbound.conf.d/pi-hole.conf, and more details on options and how to change them can be found at the Unbound configuration documentation.
$ nano /etc/unbound/unbound.conf.d/pi-hole.conf
# Add the below to the configuration file; adjust according to your needs and server capabilities
server:
num-threads: 4
# Enable operation information logging; up to 5
verbosity: 1
# Listen to queries on all interfaces
interface: 127.0.0.1
port: 5353
# Disable ipv6
do-ip6: no
# IP range authorized to send queries to DNS
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.1/32 allow
access-control: 10.10.10.0/24 allow
# Hide id.server and hostname.bind
hide-identity: yes
# Hide version.server and version.bind
hide-version: yes
# Hide addresses on the private network
private-address: 10.0.0.0/8
# A total number of unwanted replies is kept track of; when reached cache is cleared to prevent DNS Poisoning
unwanted-reply-threshold: 10000000
# Because my server has low traffic/usage I enable prefetch; this adds load but cache elements are prefetched before expiry
prefetch: yes
prefetch-key: yes
# Add minimum cache lifetime in seconds
cache-min-ttl: 1800
cache-max-ttl: 14400
# Secure DNS and use DNSSEC
harden-glue: yes
harden-dnssec-stripped: yes
Next, I restart the service and run a test query to make sure it’s resolving it.
$ systemctl restart unbound.service
$ systemctl status unbound.service
$ dig news.ycombinator.com/ @127.0.0.1 -p 5353
; <<>> DiG 9.18.28-0ubuntu0.24.04.1-Ubuntu <<>> news.ycombinator.com/ @127.0.0.1 -p 5353
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 31572
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;news.ycombinator.com/. IN A
;; AUTHORITY SECTION:
. 3563 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024120801 1800 900 604800 86400
;; Query time: 18 msec
;; SERVER: 127.0.0.1#5353(127.0.0.1) (UDP)
;; WHEN: Sun Jan 01 12:14:17 UTC 2024
;; MSG SIZE rcvd: 125
The query should now be resolved by Unbound as the SERVER
in the response gives 127.0.0.1#5353
.
With this, we’re almost done. I now turn on the Wireguard interface on the desktop as I have the configuration file prepared previously, and I added the client to the VPS’s list of peers.
This will disconnect the current SSH connection, but assuming all previous steps went OK, I am able to reconnect to the VPS server and also access http://10.10.10.1/admin to reach the admin interface for the current Pi-Hole installation.
# On my desktop I will enable the dwg0 service
$ sudo systemctl enable wg-quick@dwg0.service
# Start the dwg0 service which will create the dwg0 interface on my desktop
# (I could also name this wg0, I just opted to rename it
# to make it easier when working with multiple configs)
$ sudo systemctl start wg-quick@dwg0.service
# Check the status of the service and make sure it is active and running
$ sudo systemctl status wg-quick@dwg0.service
Now I can go to the admin interface of the Pi-Hole installation I had setup earlier and change the DNS provider from Quad9 to the instance of Unbound that runs on port 5353 on the VPS.
Once I log into the interface using the password displayed at the end of the Pi-Hole installation, I next go to Settings, select the DNS tab and here uncheck the Quad9 section in the Upstream DNS Servers part of the page. Then, I check the Custom 1 (IPv4) checkbox and enter 127.0.0.1#5353
, make sure that Bind only to interface wg0 is selected and then Save the settings.
Now I can visit DNS Leak Test and run an extended test. If everything runs as expected, the only result here should be the IP address of the VPS. If you are getting different servers showing up it means you are leaking DNS queries. There are a multitude of reasons for this, on Ubuntu derived distributions it might be down to Network Manager changing your DNS settings or you having different DNS servers configured which override the Wireguard DNS server.
This can be difficult to debug and it could take some time for you to chase it down, but some tips to get started would be to run resolvectl dns
which will show you the global DNS (if this was configured in /etc/systemd/resolved.conf) or if a global DNS is picked up from somewhere else like /run/systemd/resolve/resolved.conf or Network Manager. For Network Manager, check /etc/NetworkManager/system-connections and look for the name of your connection. The file in there will have a setting in the [ipv4]
section called dns
. This supports multiple DNS settings separated by semicolons.
There are further steps you could take and I recommend the following article by Andrea Corbellini. You can also check to see if it was indeed Pi-Hole that listened and resolved your query by running lsof -i -P -n | grep LISTEN
on the VPS and check to see if pihole is actually listening on port 53 or if there is another resolver that is using the port and responding to your queries.
While getting a configuration file from the VPS to a device with a keyboard is simple, Android and iOS devices aren’t that straightforward. To get a configuration over to a mobile device, I use qrencode
. The application creates a QR Code from a given configuration file.
# Install qrencode
$ apt install qrencode
# Create a QR Code from the previously created dwg0.conf file
$ qrencode -t ansiutf8 < dwg0.conf
# If the file was created by root and you are now signed in with a non-root account then use
$ sudo sh -c 'qrencode -t ansiutf8 < dwg0.conf'
The above command outputs a QR code to the terminal directly, which you can then scan with the Wireguard application on iOS or Android. Don’t forget that you need different configuration files for each new device you wish to add, so don’t try to reuse the same configuration file across multiple devices.
One more goal that I wanted to achieve was that of being able to access my home server resources while I’m not on my home network. The final setup is a bit clunky, but it works for now. I’d be curious about any improvements any of you out there can think of. Feel free to e-mail me with any alternatives or ideas on the vpn email address for this domain.
First, go to the Pi-Hole administrator website, select Local DNS and then DNS Records. Then, add a number of domain/IP addresses to cover your use cases. While the VPN IP address range covers 10.10.10.0/24
the local home network IP address range covers 192.168.1.0/24
. Assuming these IP ranges, I ended up with the following list, assuming the home server runs on 10.10.10.10
within the Wireguard network and 192.168.1.10
on the local network:
192.168.1.10
10.10.10.10
192.168.1.10
10.10.10.10
The home server is running a reverse proxy in front of these services, so each call gets resolved by the reverse proxy to their respective services. The VPS is already allowing traffic to port 80
, but if you are using HTTPS on any of these services, you would also need a rule to allow traffic to port 443
.
Lastly, any request coming in on the VPS wg0
interface needs to be forwarded out on the same interface, so a couple more ufw
rules need added to allow any device to easily reach these services whether they sit on the same LAN, or connect via Wireguard.
# Set the following ufw rules on the VPS - this allows forwarding of requests received on wg0 on wg0
$ ufw route allow in on wg0 out on wg0
# Set the rule to allow HTTPS traffic if your services are running with HTTPS
$ ufw allow from 10.10.10.0/24 to any port 443
$ ufw reload
An easier way to persist the rules is to add them to the PostUp
and PreDown
sections of the Wireguard configuration file. The above settings also mean that if I’m on my home network, I can use emby.home.server and access my home server with its local IP address and when I’m outside my home network, then I can access it via emby.travel.server. It’s not ideal, but this seemed to work most consistently across various OSes, devices and apps.
If after the setup, you notice that your connection isn’t that great or that you see a significant drop in connection performance, you can test the connection speed between the VPS and a client that is running Linux using iperf3
. This is a good way to check if your speed is slow because of the VPS or some other factors. I would recommend using the Cloudflare Speedtest on a client to get an idea of your current speed without the Wireguard tunnel enabled.
You can then use the speedtest-cli
application on the VPS to test its upload and download connection and then use iperf3
to test the speed of the connection between client and VPS to find any bottlenecks.
On the VPS I install iperf3
and allow connections on port 5201
to run the speed test. I then start iperf3
in server mode, which sets it up to listen for incoming requests.
# Install iperf3 if not already installed
$ apt install iperf3
# Allow connections on port 5201 from the Wireguard IP range
$ ufw allow from 10.10.10.0/24 to any port 5201
$ ufw reload
# Start iperf3 in server mode
$ iperf3 --server
# Install iperf3 on client too and then start a test by defining the ip of the VPS on the Wireguard network
# The below command will test the upload speed from client to VPS
$ iperf3 --client 10.10.10.1
Connecting to host 10.10.10.1, port 5201
[ 5] local 10.10.10.11 port 56590 connected to 10.10.10.1 port 5201
[ ID] Interval Transfer Bitrate Retr Cwnd
[ 5] 0.00-1.00 sec 1.21 MBytes 10.1 Mbits/sec 0 81.5 KBytes
[ 5] 1.00-2.00 sec 2.39 MBytes 20.1 Mbits/sec 0 195 KBytes
[ 5] 2.00-3.00 sec 2.94 MBytes 24.7 Mbits/sec 0 331 KBytes
[ 5] 3.00-4.00 sec 3.37 MBytes 28.3 Mbits/sec 0 484 KBytes
[ 5] 4.00-5.00 sec 3.86 MBytes 32.4 Mbits/sec 0 640 KBytes
[ 5] 5.00-6.00 sec 2.45 MBytes 20.5 Mbits/sec 0 786 KBytes
[ 5] 6.00-7.00 sec 3.86 MBytes 32.4 Mbits/sec 0 945 KBytes
[ 5] 7.00-8.00 sec 3.50 MBytes 29.3 Mbits/sec 0 1.08 MBytes
[ 5] 8.00-9.00 sec 3.75 MBytes 31.5 Mbits/sec 0 1.23 MBytes
[ 5] 9.00-10.00 sec 2.50 MBytes 21.0 Mbits/sec 0 1.35 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.00 sec 29.8 MBytes 25.0 Mbits/sec 0 sender
[ 5] 0.00-10.61 sec 27.6 MBytes 21.8 Mbits/sec receiver
iperf Done.
# The below command will test the download speed from VPS to the client
$ iperf3 --client 10.10.10.1 --reverse
Connecting to host 10.10.10.1, port 5201
Reverse mode, remote host 10.10.10.1 is sending
[ 5] local 10.10.10.11 port 34346 connected to 10.10.10.1 port 5201
[ ID] Interval Transfer Bitrate
[ 5] 0.00-1.00 sec 3.50 MBytes 29.4 Mbits/sec
[ 5] 1.00-2.00 sec 7.35 MBytes 61.7 Mbits/sec
[ 5] 2.00-3.00 sec 11.7 MBytes 98.0 Mbits/sec
[ 5] 3.00-4.00 sec 23.6 MBytes 198 Mbits/sec
[ 5] 4.00-5.00 sec 39.6 MBytes 332 Mbits/sec
[ 5] 5.00-6.00 sec 44.4 MBytes 372 Mbits/sec
[ 5] 6.00-7.00 sec 40.6 MBytes 340 Mbits/sec
[ 5] 7.00-8.00 sec 40.5 MBytes 340 Mbits/sec
[ 5] 8.00-9.00 sec 38.4 MBytes 322 Mbits/sec
[ 5] 9.00-10.00 sec 40.8 MBytes 342 Mbits/sec
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval Transfer Bitrate Retr
[ 5] 0.00-10.05 sec 294 MBytes 245 Mbits/sec 0 sender
[ 5] 0.00-10.00 sec 290 MBytes 244 Mbits/sec receiver
iperf Done.
In the above tests I can see that my upload speed to this test VPS is between 10 and 30 Mbps and my download speed is between 30 and 340 Mbps. After completing the test, I can stop the server on the VPS and even close down the port. For more information on tuning the performance of Wireguard, I recommend the detailed article on the Pro Custodibus website.
At this stage, you should have your VPS setup, your first two devices connected and you should also be able to access any remote resources.
Most of this could not be done without the articles below. These were great resources and I would greatly recommend them for further reading of a particular topic:
There are plenty more websites that I read through for quick fixes and I apologise for not recording those consistently, as the information there helped me fix some local issues on my Pop OS desktop.
In any case, I hope this article was useful and helped you setup your own Wireguard VPN server, access resources on your home network and provide network-wide ad-block for all your devices. By using a VPS you can generally cover a variety of devices for around $10 a month. This beats a lot of the providers out there and it does so while offering you full control over blocking lists and resources on the network.
If this guide was useful in any way, please make sure to support the Wireguard project, the Pi-Hole project, Unbound and all the other open source projects that allow us to gain some modicum of control over out digital lives!