HOW TO SET UP A WIREGUARD VPN SERVER WITH PIHOLE

2022-08-09
article cover

Introduction §

In this guide we will see how to set up a VPN server with advertisement blocking capabilities using Debian, Wireguard and PiHole(using Docker). I will update this guide from time to time to make sure that it will remain compatible with the latest version of Debian, PiHole, Docker and the NFT firewall. Below, there is a brief update history of the latest changes:

Installation §

Let us start by installing wireguard-tools:

marco@vpnnode:~$ sudo apt install wireguard
The version of NFTables I am currently using is:

marco@vpnnode:~$ sudo nft -v
nftables v1.0.6 (Lester Gooch #5)

Configure Wireguard(Server) §

Let us now start configuring the wireguard server by generating the keypair and the configuration file; to do so, we will create a reserved directory:

root@vpnnode:~# mkdir -p /etc/wireguard/
root@vpnnode:~# cd /etc/wireguard/
Once inside it, we can generate a new keypair with the wg(8) utility:

root@vpnnode:/etc/wireguard# wg genkey | tee privkey | wg pubkey > pubkey
root@vpnnode:/etc/wireguard# ls -lh
total 8.0K
-rw-r--r-- 1 root root 45 Aug  6 08:46 privkey
-rw-r--r-- 1 root root 45 Aug  6 08:46 pubkey
root@vpnnode:/etc/wireguard# cat privkey
+LoX/Rrh2VR6nFiExOweXR37HluHdOhjBiFu7jqK7mo=
at this point, copy the content of privkey file and create a new file called wg0.conf where:
  1. PrivateKey is equal to privkey;
  2. Address is the CIDR mask of the VPN(i.e. from 192.168.2.1 to 192.168.2.254);
  3. ListenPort is the UDP port where the Wireguard server will listen to.
You should have a similar structure:

root@vpnnode:/etc/wireguard# cat wg0.conf
[Interface]
PrivateKey = +LoX/Rrh2VR6nFiExOweXR37HluHdOhjBiFu7jqK7mo=
Address = 192.168.2.1/24
ListenPort = 48965

Configure the firewall §

The next step is to configure the firewall to accept incoming packets from the chosen UDP port and to create a NAT for the Wireguard interface(wg0). To keep things as simple as possible, I will be using nftables but you can accomplish the same result with pretty much any netfilter frontend.

On Debian, an empty nftables ruleset looks like this:

flush ruleset

table inet filter {
	chain input {
		type filter hook input priority filter;
	}
	chain forward {
		type filter hook forward priority filter;
	}
	chain output {
		type filter hook output priority filter;
	}
}
And, if you don't have any additional firewall rule, you will just need the following configuration:

# Wireguard NAT
add table wireguard-nat

table ip wireguard-nat {
	chain prerouting {
		type nat hook prerouting priority -100; policy accept;
	}

	chain postrouting {
		type nat hook postrouting priority 100; policy accept;
		oifname "ens3" masquerade # Be sure to replace 'ens3' with your network interface
	}
}
However, if you are planning to filter the input chain by dropping all packets and allowing only a specific set of ports, be sure to allow inbound traffic for the selected UDP port using the following rule:

chain input {
        type filter hook input priority filter; policy drop; # Drop all packets by default
        iifname "lo" accept comment "Accept loopback interface" # Accept traffic from loopback device

        # Accept established and related packets
        ct state established,related counter accept comment "Accept established and related packets"
        ct state invalid counter drop comment "Drop invalid packets"

        # Allow ICMP requests
        icmp type echo-request counter accept comment "Accept incoming ICMP"

        # Allow inbound HTTP and HTTPS traffic
        tcp dport { 80, 443 } counter accept comment "Accept incoming services"
        # Allow inbound UDP traffic for Wireguard
        udp dport { 48965 } counter accept comment "Accept Wireguard packets"
}
However, if you are planning to filter the input chain by dropping all packets and allowing only a specific set of ports, be sure to allow inbound traffic for the selected UDP port using the following rule:

chain input {
        type filter hook input priority filter; policy drop; # Drop all packets by default
        iifname "lo" accept comment "Accept loopback interface" # Accept traffic from loopback device

        # Accept established and related packets
        ct state established,related counter accept comment "Accept established and related packets"
        ct state invalid counter drop comment "Drop invalid packets"

        # Allow ICMP requests
        icmp type echo-request counter accept comment "Accept incoming ICMP"

        # Allow inbound HTTP and HTTPS traffic
        tcp dport { 80, 443 } counter accept comment "Accept incoming services"
        # Allow inbound UDP traffic for Wireguard
        udp dport { 48965 } counter accept comment "Accept Wireguard packets"
}
After that, we can reload the ruleset by issuing the following command:

$> sudo systemctl restart nftables
Routing is complete, the last thing to do is to enable ip forwarding

Enable IP forwarding §

In order to route packets between VPN's clients and a remote host, we need to enable the ip forwarding feature. To do so, type the following command:

root@vpnnode:/etc/wireguard# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
and to make it permanent, edit the /etc/sysctl.d/99-sysctl.conf file and uncomment the following line:

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1

Configure Wireguard(Client) §

Wireguard can be installed in a wide spectrum of operating system, in this guide I will not cover the installation process; in order to install the wireguard client for your computer/table/phone, please refer to this page. After that, open up the configuration file and add the following content:

[Interface]
PrivateKey = ni16f/oyWn8G0rdsJ7YGyytjXvJSfaNzhzFSG5Bv4Gg= # <-- client private key
Address = 192.168.2.2/24
DNS = 192.168.2.1

[Peer]
PublicKey = 4wzgj/0u53Jiheq8DjwQ9GRnvnzv0qcsisKARdnrr1c= # <-- server public key
PresharedKey = PW21sz8kl+nY8WRNJEypkqWJGLARSX2A5KjbPfaEUp0= # <-- wg genpsk
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <SERVER_IP_ADDRESS>:48965
PersistentKeepalive = 15
Be sure to replace the following fields according to your needs:
  1. PrivateKey: replace it with client's private key(you can generate a new keypair using wg genkey | tee privkey | wg pubkey > pubkey command if you do not use a graphical client);
  2. PublicKey: replace with server's public key(i.e. /etc/wireguard/pubkey file on the VPN server);
  3. PresharedKey: you can generate a preshared key with wg genpsk(this field is optional);
  4. Endpoint: the IP address of your server with the Wireguard UDP port.
Let us now complete the configuration by adding a new client in the server configuration file.

Back to the server §

Open the /etc/wireguard/wg0.conf file and add the following entry at the end:

[Interface]
PrivateKey = +LoX/Rrh2VR6nFiExOweXR37HluHdOhjBiFu7jqK7mo=
Address = 192.168.2.1/24
ListenPort = 48965

# Add this
[Peer]
PublicKey = 1+54fGF/zZlVTxDiJ3rlmrH65+5K1NMFKwxlniA/2js= # <-- Client public key
PresharedKey = PW21sz8kl+nY8WRNJEypkqWJGLARSX2A5KjbPfaEUp0=
AllowedIPs = 192.168.2.2/32
where:
  1. PublicKey is the public key of the client;
  2. PresharedKey is the preshared key previously generated on the client configuration;
  3. AllowedIPs is the client's IP address.
This means that every time you want to add a new client to the network, you simply create a new keypair, add the public key to the server configuration file and restart the Wireguard network interface. Just be sure to assign a unique IP address to each client.

Finally, let us start the Wireguard server:

root@vpnnode:/etc/wireguard# systemctl enable wg-quick@wg0 --now
root@vpnnode:/etc/wireguard# wg
interface: wg0
public key: 4wzgj/0u53Jiheq8DjwQ9GRnvnzv0qcsisKARdnrr1c=
private key: (hidden)
listening port: 48965

peer: 1+54fGF/zZlVTxDiJ3rlmrH65+5K1NMFKwxlniA/2js=
preshared key: (hidden)
allowed ips: 192.168.2.2/32
You can now enable the VPN connection from your client. Right now, you will only be able to ping the VPN gateway(192.168.2.1) without being able to access the internet. This is normal because the DNS server we have specified in the client's configuration file(i.e. 192.168.2.1) is not yet active. We will fix this in a second by installing PiHole.

Configuring PiHole §

PiHole is an internet tracking blocking system which acts as a DNS sinkhole. It is designed primarily for embedded devices such as the RaspberryPi, but it can be easily installed on any other Linux operating system. Since PiHole makes use of many different daemons(such as a DNS server, lighttpd and the AdminLTE dashboard), we will install it using Docker. This approach allows us to avoid manual configuration and simplify update operations.

Installing Docker on Debian is outside the scope of this guide, so please refer to the official documentation.

After that, set up a password for the PiHole dashboard(i.e. the password needed to log into the web interface) and configure the docker container with the following docker-compose.yml file:

version: "3"

# More info at https://github.com/pi-hole/docker-pi-hole/ and https://docs.pi-hole.net/
services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    # For DHCP it is recommended to remove these ports and instead add: network_mode: "host"
    ports:
      - "192.168.2.1:53:53/tcp"
      - "192.168.2.1:53:53/udp"
      - "192.168.2.1:8888:80/tcp"
    dns:
      - 127.0.0.1
      - 1.1.1.1
    environment:
      TZ: 'Europe/Rome'
      FTLCONF_webserver_api_password: 'BADPW' # Set your password here
    # Volumes store your data between container upgrades
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped
Launch it with docker-compose up -d, you should see its status in the docker logs:

marco@vpnnode:~$ docker-compose ps
 Name    Command       State                                                     Ports                                               
-------------------------------------------------------------------------------------------------------------------------------------
pihole   start.sh   Up (healthy)   123/udp, 443/tcp, 192.168.2.1:53->53/tcp, 192.168.2.1:53->53/udp, 67/udp, 192.168.2.1:8888->80/tcp
Last but not least, we need to configure PiHole to make it listen to the Wireguard network interface. We can do that from the PiHole admin page(be sure to have your VPN turned on before accessing the PiHole gateway). You should have a page like this:

login pihole Enter your admin password and follow these instructions:
  1. Open the "Settings" drop-down menu from the left pane;
  2. Click on "All settings";
  3. Scroll down to the dns.listeningMode and select the SINGLE option;
  4. Click "Save & Apply" button on the bottom right corner.
network config

After that, you can choose a different upstream DNS server from the "DNS" menu on the left pane, for instance the Cloudflare DNS or the OpenDNS.

Conclusions §

At this point you should be able to reach the internet. Let us try our new VPN on some sites(apart from whatismyipaddress.com).

Go to this page if you are using Cloudflare's DNS: cloudflare dns test

This page checks whether our ads blocking system works(you can also try to open any newspaper website with adblock disabled...): adblocks testing

This website checks whether your VPN/DNS leaks your real IP address. Be sure to restart your network daemon(i.e., NetworkManager on Linux) before starting this test to avoid false-positive results. dnsleak test

Finally, this website checks whether our DNS is vulnerable to DNS amplification attacks. This kind of vulnerabilities occour when a misconfigured DNS server acts as an open resolver; meaning that it responds to queries from any source and from any interface. DNS amplification attacks can be mitigated by properly configuring your firewall to restrict DNS traffic only to trusted sources. When everything is properly configured, you should get a result similar to this:

dns amplification attacks