Block online ads, trackers and malware with Pi-hole, WireGuard, DoT and DoH servers

By Rahul Pandit

Posted on Monday, 10 March 2025

Last updated on Tuesday, 15 April 2025

ToC

Introduction

In late February 2025, I executed innocuous-sounding pihole -up command on my server to update my existing installation of Pi-hole which had been serving me faithfully for the last 4 years. The amazing developers of Pi-hole project had just released the much awaited version 6 just a few hours prior. What I didn't know before executing that fateful command was that the update was going to be a botched one. It slowed my server to a crawl. The updated Pi-hole couldn't handle the size of my blocklisted domains which is in the millions and the web interface took ages to load.

I, therefore, decided to nuke the server and start afresh. This blog post explains how I did it.

What we'll do in this blog post is as follows: I'm assuming you have a brand new VPS or a bare-metal server with you. We will set it up and install:

  • dnscrypt-proxy: It will act as upstream for Pi-hole. It will pass DNS queries coming from Pi-hole to further upstream which will be encrypted DNS servers.
  • Pi-hole: It will block DNS queries of bad domains and pass the remaining ones to dnscrypt-proxy.
  • WireGuard VPN: Our clients will securely and privately pass DNS queries to Pi-hole via encrypted WireGuard tunnel. We will also visit Pi-hole dashboard via this tunnel.
  • (Optional) Nginx for DoT (DNS-over-TLS): Our clients can also securely and privately pass DNS queries to Pi-hole via our own DoT server.
  • (Optional) dnsdist for DoH (DNS-over-HTTPS): Our clients can also securely and privately pass DNS queries to Pi-hole via, guess what, our own DoH server.

Prerequisites

I'm assuming you have a VPS or a bare-metal server with Ubuntu 24.04 installed on it and a static IP attached to it. Since Pi-hole installed on this server will act like a DNS server for all your devices, it is imperative that the server is located near your physical location. The closer the server is to your location, the lower the latency will be and vice versa.

If you want to deploy a DoH and/or DoT server as well, you will need a domain name in your possession. I'll assume it is example.com. You'll need to create two subdomains for it as follows: dot.example.com for DoT and doh.example.com for DoH and point A records of them at the IPv4 address of your server and point AAAA records of them at the IPv6 address of your server if it exists.

Initial Setup

If you just got the server, you can follow instructions given in another one of my blog post Initial Server Setup and then come back here.

Make sure UFW firewall is enabled. Make sure port 53 is NOT accessible by the world.

sudo ufw limit ssh
sudo ufw enable
sudo ufw status verbose

Install dnscrypt-proxy

We start by installing dnscrypt-proxy first.

sudo apt install dnscrypt-proxy

Take backup of current configuration file.

sudo mv /etc/dnscrypt-proxy/dnscrypt-proxy.toml /etc/dnscrypt-proxy/dnscrypt-proxy.toml.backup

Copy example configuration file as the new configuration file.

sudo cp /usr/share/doc/dnscrypt-proxy/examples/example-dnscrypt-proxy.toml /etc/dnscrypt-proxy/dnscrypt-proxy.toml

Edit /etc/dnscrypt-proxy/dnscrypt-proxy.toml and make the changes in the lines which are given below:

# Complete list of servers is available here: https://dnscrypt.info/public-servers/
# You can remove the providers you don't like. You can add more providers from the link above.
# dnscrypt-proxy will automatically pick fastest working servers from this list.
# Note that the require_* filters do NOT apply when using this setting.
server_names = ['nextdns', 'nextdns-ipv6', 'cloudflare', 'cloudflare-ipv6', 'cloudflare-security', 'cloudflare-security-ipv6', 'mullvad-doh', 'mullvad-adblock-doh', 'mullvad-base-doh', 'adguard-dns', 'adguard-dns-ipv6', 'adguard-dns-doh', 'adguard-dns-doh-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-doh', 'cleanbrowsing-security-ipv6', 'controld-block-malware-ad', 'rethinkdns-doh', 'quad9-dnscrypt-ip4-filter-pri', 'quad9-dnscrypt-ip6-filter-pri', 'quad9-doh-ip4-port443-filter-pri', 'quad9-doh-ip6-port443-filter-pri', ]

# Empty listen_addresses to use systemd socket activation
listen_addresses = []

ipv4_servers = true

# Use servers reachable over IPv6 -- Do not enable if you don't have IPv6 connectivity
# ipv6_servers = true

dnscrypt_servers = true

doh_servers = true

require_dnssec = true

require_nolog = true

# sometimes I want filtered lists (which filter ads and trackers)
# require_nofilter = true

# following option increases privacy AND server CPU usage. Uncomment wisely.
# dnscrypt_ephemeral_keys = true

# following option increases privacy AND latency. Uncomment wisely.
# tls_disable_session_tickets = true

By default, dnscrypt-proxy will run on port 53. But we want Pi-hole to run on port 53. So, edit dnscrypt-proxy socket's systemd config file to make it run on port 5335. Run command sudo systemctl edit --full dnscrypt-proxy.socket and make the changes as follows:

[Socket]
ListenStream=127.0.2.1:5335
ListenDatagram=127.0.2.1:5335

Restart dnscrypt-proxy and dnscrypt-proxy.socket.

sudo systemctl restart dnscrypt-proxy
sudo systemctl restart dnscrypt-proxy.socket

Verify that dnscrypt-proxy is running on port 5335.

sudo systemctl status dnscrypt-proxy
sudo ss -lntup

Run some DNS queries using dnscrypt-proxy.

dig +short example.com @127.0.2.1 -p 5335

Install Pi-hole

Download Pi-hole installation script.

wget -O pihole.sh https://install.pi-hole.net

Run it. During the installation process, it will ask you to select upstream DNS server, select custom option and set 127.0.2.1#5335 (which is dnscrypt-proxy) as the custom upstream DNS.

bash pihole.sh

Set/change password ASAP.

sudo pihole setpassword

Now, we make sure upstream is correctly set. We want port 80 free for certbot, that's why we change the port here to 8080. And we increase Pi-hole's admin dashboard's session timeout value to 1 week in seconds (for convenience). Edit /etc/pihole/pihole.toml and make the changes in dns and webserver sections as given below.

[dns]
  upstreams = [
    "127.0.2.1#5335"
  ]
[webserver]
  port = "localhost:8080"
  [webserver.session]
    timeout = 604800

Restart Pi-hole and make sure it's running on port 53.

sudo systemctl restart pihole-FTL
sudo ss -lntup

Run some DNS queries using Pi-hole.

dig +short example.com @127.0.0.1
dig +short pixel.facebook.com @127.0.0.1

Don't forget to periodically take backup of Pi-hole's configuration.

sudo pihole-FTL --teleporter

Restart your server.

sudo reboot

Install WireGuard

Install WireGuard and qrencode packages. qrencode will be used to create QR code of WireGuard configuration of a client. This QR code can then be scanned using the WireGuard app on your phone.

sudo apt install wireguard qrencode

First, we generate a key-pair (a private key and a public key) for the server.

We create a private key for the server and limit the file permissions :

wg genkey | sudo tee /etc/wireguard/server.key
sudo chmod go= /etc/wireguard/server.key

Then, we create the corresponding public key for the server:

sudo cat /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub

We will use 10.100.0.0/24 as our network. We will use 10.100.0.1 as server's IPv4 address and fd08:4711::1 as server's IPv6 address.

Add server details (IP addresses and port) to WireGuard configuration file /etc/wireguard/wg0.conf.

[Interface]
Address = 10.100.0.1/24, fd08:4711::1/64
ListenPort = 51820

Add private key content in there too.

echo "PrivateKey = $(sudo cat /etc/wireguard/server.key)" | sudo tee -a /etc/wireguard/wg0.conf

To add a new WireGuard client automatically, I wrote a little script heavily inspired by the one given in Pi-hole's documentation for setting up WireGuard.

Create a new file in your home directory named wireguard-generate-client-config.sh and add following content to it.

# Execute this on the server to generate a new wireguard client config
# bash wireguard-generate-client-config.sh 51820 clientname 2

# public ip of server
server_ip="$(curl -s4 https://ifconfig.me)"
# the port wireguard will listen on
port="$1"
# generate a key-pair for the client (no spaces or special characters in the name!  only alphanumeric and _ are allowed!)
name="$2"
# increment the number below by 1 EVERY TIME YOU CREATE A NEW CLIENT!
number="$3"

echo "server ip: $server_ip"
echo port: $port
echo client name: ${name}
echo "number: ${number}"

# create public key of the client
wg genkey | sudo tee "/etc/wireguard/${name}.key" | wg pubkey | sudo tee "/etc/wireguard/${name}.pub"

# symmetric-key cryptography to be mixed into the already existing public-key cryptography for post-quantum resistance
wg genpsk | sudo tee "/etc/wireguard/${name}.psk"

# Add client to server config
echo "" | sudo tee -a /etc/wireguard/wg0.conf
echo "[Peer]" | sudo tee -a /etc/wireguard/wg0.conf
echo "PublicKey = $(sudo cat /etc/wireguard/${name}.pub)" | sudo tee -a /etc/wireguard/wg0.conf
echo "PresharedKey = $(sudo cat /etc/wireguard/${name}.psk)" | sudo tee -a /etc/wireguard/wg0.conf
# first client will have the IP addresses 10.100.0.2/32 and fd08:4711::2/128 and so on.
# Make sure to increment the IP address for any further client!
echo "AllowedIPs = 10.100.0.${number}/32, fd08:4711::${number}/128" | sudo tee -a /etc/wireguard/wg0.conf

# Create client config file
echo "[Interface]" | sudo tee /etc/wireguard/${name}.conf
# first client will have the IP addresses 10.100.0.2/32 and fd08:4711::2/128
# RIGHTMOST VALUE IN THE IP ADDRESSES NEEDS TO BE INCREMENTED BY 1 FOR THE NEXT CLIENT!
echo "Address = 10.100.0.${number}/32, fd08:4711::${number}/128" | sudo tee -a /etc/wireguard/${name}.conf
# Your Pi-hole's IP
echo "DNS = 10.100.0.1" | sudo tee -a /etc/wireguard/${name}.conf
# Add private key of this client
echo "PrivateKey = $(sudo cat /etc/wireguard/${name}.key)" | sudo tee -a /etc/wireguard/${name}.conf
# Add your server as peer to this client config:
echo "" | sudo tee -a /etc/wireguard/${name}.conf
echo "[Peer]" | sudo tee -a /etc/wireguard/${name}.conf
# Add public ip of the server
echo "Endpoint = ${server_ip}:${port}" | sudo tee -a /etc/wireguard/${name}.conf
echo "AllowedIPs = 10.100.0.1/32, fd08:4711::1/128" | sudo tee -a /etc/wireguard/${name}.conf
# Add the public key of the server as well as the PSK for this connection:
echo "PublicKey = $(sudo cat /etc/wireguard/server.pub)" | sudo tee -a /etc/wireguard/${name}.conf
echo "PresharedKey = $(sudo cat /etc/wireguard/${name}.psk)" | sudo tee -a /etc/wireguard/${name}.conf

# Restart wireguard so it picks up the changes.
sudo systemctl restart wg-quick@wg0

# show wireguard status
sudo wg

# Display the client configuration as QR code (to be scanned by your client, for example, your mobile phone)
sudo qrencode -t ansiutf8 -r /etc/wireguard/${name}.conf

Run the script to generate your first client configuration

bash wireguard-generate-client-config.sh 51820 myphone 2

Run the script again to generate your second client configuration (don't forget to change client name and increment the number at the end by 1 every time!)

bash wireguard-generate-client-config.sh 51820 mytv 3

And so on.

Go to your VPS provider's dashboard and allow UDP traffic to come in on port 51820. Then do the same on your server using UFW.

sudo ufw allow in 51820/udp comment 'for wireguard'

To allow DNS queries from WireGuard clients in 10.100.0.0/24 subnet to reach Pi-hole listening on port 53, add these UFW rules:

sudo ufw allow proto tcp from 10.100.0.0/24 to 10.100.0.1 port 53 comment 'for wg pihole dns tcp'
sudo ufw allow proto udp from 10.100.0.0/24 to 10.100.0.1 port 53 comment 'for wg pihole dns udp'

To allow HTTP requests from WireGuard clients in 10.100.0.0/24 subnet to reach Pi-hole webserver listening on port 8080, add the following UFW rule:

sudo ufw allow proto tcp from 10.100.0.0/24 to 10.100.0.1 port 8080 comment 'for wg pihole http'

Make Pi-hole webserver listen on wg0 interface (in addition to the localhost interface). Edit /etc/pihole/pihole.toml and make following change:

[webserver]
  port = "localhost:8080,10.100.0.1:8080"

Restart Pi-hole.

sudo systemctl restart pihole-FTL

Append your clientname to IP address mappings to /etc/hosts file or /etc/cloud/templates/hosts.debian.tmpl file (depends on your distro) to have client names show up in Pi-hole admin dashboard.

10.100.0.2 myphone
10.100.0.3 mytv

You'll also have to reboot your server.

sudo reboot

To remove a WireGuard client from the server :

  • Remove appropriate peer from /etc/wireguard/wg0.conf
  • Delete these 4 files :
    • /etc/wireguard/client_name.conf
    • /etc/wireguard/client_name.pub
    • /etc/wireguard/client_name.key
    • /etc/wireguard/client_name.psk
  • Restart wireguard: sudo systemctl restart wg-quick@wg0

Set up WireGuard on clients

On Android phone:

  • Install WireGuard application from Google Play Store or F-Droid.
  • Open WireGuard -> Click on + button -> Scan from QR Code -> Then scan the QR code you generated when you ran the wireguard-generate-client-config.sh script on the server.
  • Start the tunnel.
  • Exit the WireGuard app.
  • Go to Settings -> Network and Internet -> VPN -> Select WireGuard -> turn ON the Always-On option. Make sure 'Block connection outside the tunnel' is OFF.

On Google TV:

  • Generate WireGuard config for your TV named something like mytv.conf.
  • Make sure your computer and Google TV are on the same local network.
  • Enable developer options in your TV.
  • Enable USB debugging.
  • Install adb in your computer.
  • Execute following commands on your computer:
    • adb devices
    • adb connect <ip-address-of-smart-tv-in-your-lan>:5555
    • A dialog box will pop up on your tv to let your computer access your TV, select 'Allow'.
    • adb devices
    • adb push mytv.conf /sdcard/Download/
    • adb disconnect
  • Install WireGuard and a file manager in your TV from Google Play Store.
  • Run WireGuard on your TV and choose mytv.conf as a file to create new tunnel.
  • Start the tunnel.

Instructions for other clients coming soon!

Access Pi-hole dashboard

You can access it securely and privately in two ways.

  • Using SSH tunnel from your local machine:

    • Execute on local machine: ssh -C -N -L 8000:localhost:8080 server
    • Open browser and visit http://localhost:8000/admin and log in.
  • If you have WireGuard client configuration imported on your device, open browser and visit http://pi.hole:8080/admin or http://10.100.0.1:8080/admin and log in.

Look around and explore Pi-hole dashboard. Add more blocklists given in my other blog post: Good Pi-hole blocklists that stop online ads, trackers and malware.

Congratulations!

If you followed the blog post till here, you now have a working Pi-hole+WireGuard combo in your service which blocks ads, trackers and malware wherever you go! Congrats!

The stuff that follows is strictly optional. Do it if you want DNS-over-TLS or DNS-over-HTTPS server too.

Install Nginx for DNS-over-TLS

Log in to your DNS registrar and point a domain or a subdomain to your server's public IP address using A record first. If your server has IPv6 address, set up AAAA record too. I'll assume it is dot.example.com.

Install Nginx. We will use Nginx to handle DNS-over-TLS queries.

sudo apt install nginx nginx-extras

Remove the default site.

sudo rm /etc/nginx/sites-enabled/default
sudo systemctl restart nginx

Allow TCP traffic to come in from port 80 in the dashboard of your VPS provider. Then do the same using UFW. Certbot needs this to issue SSL certificates :

sudo ufw allow 80/tcp comment 'for certbot'

DNS-over-TLS uses port 853. Allow TCP traffic to come in from port 853 in the dashboard of your VPS provider. Then do the same using UFW :

sudo ufw allow 853/tcp comment 'for dot'

Install certbot on your server. We will need it to issue SSL certificate for our domain.

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

Request a certificate for your domain dot.example.com:

sudo certbot certonly --nginx -d dot.example.com

Make sure renewal can happen by simulating the renewal process.

sudo certbot renew --dry-run

Following code defines Pi-hole as upstream DNS and creates a server block which takes in DNS-over-TLS queries and forwards them to the aforementioned upstream. Append the following code at the very end of the /etc/nginx/nginx.conf file :

stream {
    # DNS upstream pool
    upstream piholedns {
        zone dns 64k;
        server 127.0.0.1:53;
    }

    # DoT server
    server {
        listen 853 ssl;
        listen [::]:853 ssl;

        ssl_certificate /etc/letsencrypt/live/dot.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/dot.example.com/privkey.pem;
        ssl_trusted_certificate /etc/letsencrypt/live/dot.example.com/chain.pem;

        proxy_pass piholedns;
    }
}

Make sure there are no errors in the nginx configuration and then restart nginx :

sudo nginx -t
sudo systemctl restart nginx

From local machine, execute following commands to check whether your server can successfully handle DoT requests.

dig +short +tls pi-hole.net @dot.example.com
dig +short +tls pixel.facebook.com @dot.example.com

Set up DoT on clients

On Android :

  • Open Settings -> Network and Internet -> Private DNS -> Select 'Private DNS provider hostname' -> Enter dot.example.com

Instructions for other clients coming soon!

Install dnsdist for DNS-over-HTTPS

Log in to your DNS registrar and point a domain or a subdomain to your server's public IP address using A record first. If your server has IPv6 address, set up AAAA record too. I'll assume it is doh.example.com.

To handle DNS-over-HTTPS queries, we will use DNSDist. There is an alternative called doh-server but it's not available in Ubuntu 24.04 official repos yet so you'll have to either use their binary or compile it yourself.

By the way, I'm using nginx as a reverse proxy in front of dnsdist because we're already using it for DoT and I didn't want the hassle of configuring SSL for dnsdist.

DNS-over-HTTPS uses port 443. Allow TCP traffic to come in from port 443 in the dashboard of your VPS provider. Then do the same using UFW :

sudo ufw allow 443/tcp comment 'for doh'

Delete everything in /etc/dnsdist/dnsdist.conf file and copy following content in it:

-- who can access the server
setACL({"<YOUR_PUBLIC_IPV4_IP_ADDRESS>", "<YOUR_PUBLIC_IPV6_IP_ADDRESS>"})

-- start DoH server at port 5300
addDOHLocal("127.0.0.1:5300", nil, nil, "/dns-query", { reusePort=true })

-- forward all dns queries to 127.0.0.1:53 which is Pi-hole
pihole = newServer({address="127.0.0.1:53", name="piholedns"})

-- following line disables 1 query/second for health-check
pihole:setUp()

-- disable security status polling via DNS
setSecurityPollSuffix("")

Make sure there are no errors in dnsdist configuration and restart it. dnsdist will say that it is running in DNS over HTTP mode instead of DNS over HTTPS which is to be expected since we will terminate SSL in Nginx itself.

sudo dnsdist --check-config
sudo systemctl restart dnsdist

Request a certificate for your domain doh.example.com:

sudo certbot certonly --nginx -d doh.example.com

Make sure renewal can happen by simulating the renewal process.

sudo certbot renew --dry-run

Time to add a new nginx site configuration file /etc/nginx/sites-available/doh.example.com as follows:

upstream dnsdist {
    zone dohd 64k;
    server 127.0.0.1:5300;
}

server {
    listen 0.0.0.0:443 ssl http2;
    listen [::]:443 ssl http2;

    server_name doh.example.com;
    ssl_certificate /etc/letsencrypt/live/doh.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/doh.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/doh.example.com/chain.pem;

    location /dns-query {
        proxy_pass http://dnsdist;
    }

    location / {
        return 404 "404 Not Found\n";
    }
}

Create a symbolic link to the file above in /etc/nginx/sites-enabled/ directory:

sudo ln -s /etc/nginx/sites-available/doh.example.com /etc/nginx/sites-enabled

Make sure there are no errors in the Nginx configuration and then restart it :

sudo nginx -t
sudo systemctl restart nginx

From local machine, execute following commands to check whether your server can successfully handle DoH requests.

dig +short +https pi-hole.net @doh.example.com
dig +short +https pixel.facebook.com @doh.example.com

curl -IL --doh-url https://doh.example.com/dns-query https://pi-hole.net

Set up DoH on clients

  • Open Firefox -> Open Settings -> Privacy and Security -> DNS over HTTPS -> Select 'Max Protection' -> Choose Provider -> Select 'Custom' -> Enter https://doh.example.com/dns-query

  • Open Chromium or any Chromium-based browser like Chrome/Brave/Edge/etc: Open Settings -> Privacy and security -> Security -> Enable 'Use secure DNS' -> Select 'With' -> Select 'Custom' -> Enter https://doh.example.com/dns-query

Instructions for other clients coming soon!

References



Cover Picture Credit : Photo by Anwaar Ali on Unsplash





Recent Posts

Deploy Vaultwarden password manager, Portainer, Nginx and Certbot in Docker


Good Pi-hole blocklists that stop online ads, trackers and malware


Block online ads, trackers and malware with Pi-hole, WireGuard, DoT and DoH servers


Free third-party DNS for blocking ads and trackers


My Chess Notes