Set up a secure WireGuard VPN server with integrated Pi-hole DNS filtering and Unbound recursive resolver for ad blocking and privacy protection. This configuration provides secure remote access while filtering malicious domains and advertisements.
Prerequisites
- Root or sudo access
- Static IP address or DDNS
- Firewall access to configure port 51820
- Basic understanding of DNS concepts
What this solves
A WireGuard VPN with Pi-hole and Unbound creates a secure tunnel that filters ads, trackers, and malicious domains at the DNS level. This setup provides privacy protection for remote devices while maintaining high performance through WireGuard's modern cryptography and Unbound's recursive DNS resolution.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest security patches and package versions.
sudo apt update && sudo apt upgrade -y
Install required packages
Install WireGuard, Unbound recursive DNS resolver, and curl for Pi-hole installation.
sudo apt install -y wireguard unbound curl qrencode iptables-persistent
Configure Unbound recursive DNS resolver
Configure Unbound to handle DNS recursion and validation for improved privacy and performance.
server:
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip6: no
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
num-threads: 1
so-rcvbuf: 1m
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
Start and enable Unbound
Enable Unbound to start on boot and verify it's running on the correct port.
sudo systemctl enable --now unbound
sudo systemctl status unbound
dig @127.0.0.1 -p 5335 example.com
Install Pi-hole
Download and run the Pi-hole installation script with automated configuration.
curl -sSL https://install.pi-hole.net | bash
During installation, select the following options:
- Choose your network interface
- Select "Custom" for upstream DNS and enter 127.0.0.1#5335
- Install the web admin interface
- Install lighttpd web server
- Enable query logging
Configure Pi-hole DNS settings
Set Pi-hole to use Unbound as the upstream DNS resolver for better privacy.
sudo pihole -a -p
sudo pihole restartdns
Access the Pi-hole web interface to verify the configuration:
echo "Pi-hole admin interface: http://$(hostname -I | awk '{print $1}')/admin/"
Add additional blocklists
Configure Pi-hole with comprehensive blocklists for enhanced ad blocking and security.
sudo pihole -w -l << 'EOF'
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://someonewhocares.org/hosts/zero/hosts
https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/BaseFilter/sections/adservers.txt
https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&mimetype=plaintext
EOF
sudo pihole -g
Generate WireGuard server keys
Create the cryptographic keys needed for the WireGuard server configuration.
sudo mkdir -p /etc/wireguard
cd /etc/wireguard
sudo wg genkey | sudo tee server_private_key | sudo wg pubkey | sudo tee server_public_key
sudo chmod 600 server_private_key
Configure WireGuard server
Create the main WireGuard server configuration with Pi-hole DNS integration.
[Interface]
PrivateKey = $(sudo cat /etc/wireguard/server_private_key)
Address = 10.8.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o $(ip route | awk '/default/ { print $5 }' | head -1) -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o $(ip route | awk '/default/ { print $5 }' | head -1) -j MASQUERADE
DNS = 10.8.0.1
Client configurations will be added here
Enable IP forwarding
Configure the system to forward packets between the VPN and external networks.
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Configure Pi-hole to listen on WireGuard interface
Modify Pi-hole configuration to accept DNS queries from VPN clients.
sudo sed -i 's/PIHOLE_INTERFACE=.*/PIHOLE_INTERFACE=eth0,wg0/' /etc/pihole/setupVars.conf
sudo pihole restartdns
Create client configuration script
Create a script to easily generate client configurations with proper DNS settings.
#!/bin/bash
CLIENT_NAME="$1"
if [ -z "$CLIENT_NAME" ]; then
echo "Usage: $0 "
exit 1
fi
SERVER_PUBLIC_KEY=$(sudo cat /etc/wireguard/server_public_key)
SERVER_IP=$(curl -s ifconfig.me || curl -s icanhazip.com)
CLIENT_PRIVATE_KEY=$(wg genkey)
CLIENT_PUBLIC_KEY=$(echo $CLIENT_PRIVATE_KEY | wg pubkey)
CLIENT_IP="10.8.0.$(($(sudo wg show wg0 2>/dev/null | grep -c peer) + 2))"
Add client to server config
echo "" | sudo tee -a /etc/wireguard/wg0.conf
echo "# $CLIENT_NAME" | sudo tee -a /etc/wireguard/wg0.conf
echo "[Peer]" | sudo tee -a /etc/wireguard/wg0.conf
echo "PublicKey = $CLIENT_PUBLIC_KEY" | sudo tee -a /etc/wireguard/wg0.conf
echo "AllowedIPs = $CLIENT_IP/32" | sudo tee -a /etc/wireguard/wg0.conf
Generate client config
cat > "/tmp/${CLIENT_NAME}.conf" << EOF
[Interface]
PrivateKey = $CLIENT_PRIVATE_KEY
Address = $CLIENT_IP/32
DNS = 10.8.0.1
[Peer]
PublicKey = $SERVER_PUBLIC_KEY
AllowedIPs = 0.0.0.0/0
Endpoint = $SERVER_IP:51820
PersistentKeepalive = 25
EOF
echo "Client configuration saved to /tmp/${CLIENT_NAME}.conf"
echo "QR code for mobile devices:"
qrencode -t ansiutf8 < "/tmp/${CLIENT_NAME}.conf"
Restart WireGuard to apply changes
sudo systemctl restart wg-quick@wg0
sudo chmod 755 /usr/local/bin/wg-client
Configure firewall rules
Set up firewall rules to allow WireGuard traffic and protect the server.
sudo ufw allow 51820/udp comment 'WireGuard'
sudo ufw allow 80/tcp comment 'Pi-hole HTTP'
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw --force enable
Start WireGuard server
Enable and start the WireGuard VPN server with the configured interface.
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo systemctl status wg-quick@wg0
Create your first VPN client
Generate a client configuration file for connecting to your VPN.
sudo wg-client laptop
cat /tmp/laptop.conf
Configure custom DNS filtering rules
Add custom domains to Pi-hole blocklist and whitelist for fine-tuned filtering.
# Block additional domains
sudo pihole -b malicious-domain.com tracking-service.net
Whitelist legitimate services that might be blocked
sudo pihole -w legitimate-service.com cdn-provider.net
View current lists
sudo pihole -q -exact blocked-domain.com
Verify your setup
Test your WireGuard VPN and DNS filtering configuration.
# Check WireGuard status
sudo wg show
sudo systemctl status wg-quick@wg0
Verify Pi-hole is blocking ads
dig @10.8.0.1 doubleclick.net
nslookup ads.google.com 10.8.0.1
Test Unbound recursive resolution
dig @127.0.0.1 -p 5335 example.com
Check Pi-hole query logs
tail -f /var/log/pihole.log
Verify firewall rules
sudo ufw status verbose
On a connected client, test DNS filtering:
# Should be blocked by Pi-hole
nslookup doubleclick.net
Should resolve normally
nslookup example.com
Check your external IP (should show VPN server IP)
curl ifconfig.me
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Client can't connect to VPN | Firewall blocking WireGuard port | sudo ufw allow 51820/udp |
| DNS queries not filtered | Pi-hole not listening on VPN interface | Add wg0 to PIHOLE_INTERFACE in setupVars.conf |
| No internet through VPN | IP forwarding disabled | echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf && sudo sysctl -p |
| Unbound not resolving | Wrong port configuration | Verify Unbound runs on port 5335: sudo netstat -tulpn | grep 5335 |
| Pi-hole web interface unreachable | Lighttpd not running | sudo systemctl restart lighttpd |
| VPN connected but slow DNS | DNS timeout issues | Check Pi-hole logs: tail -f /var/log/pihole.log |
Next steps
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
SCRIPT_NAME=$(basename "$0")
UNBOUND_PORT=5335
PIHOLE_USER="pihole"
# Usage function
usage() {
echo "Usage: $SCRIPT_NAME [options]"
echo "Options:"
echo " -h, --help Show this help message"
echo " --no-confirm Skip confirmation prompts"
echo ""
echo "This script installs and configures WireGuard VPN with Pi-hole and Unbound"
exit 1
}
# Logging functions
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Script failed. Cleaning up..."
systemctl stop unbound 2>/dev/null || true
systemctl disable unbound 2>/dev/null || true
if command -v pihole >/dev/null 2>&1; then
pihole uninstall --unattended 2>/dev/null || true
fi
fi
exit $exit_code
}
trap cleanup ERR
# Parse arguments
NO_CONFIRM=false
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) usage ;;
--no-confirm) NO_CONFIRM=true ;;
*) log_error "Unknown option: $1"; usage ;;
esac
shift
done
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
# Detect distribution
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
IPTABLES_PKG="iptables-persistent"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
IPTABLES_PKG="iptables-services"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
IPTABLES_PKG="iptables-services"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
log_info "Detected distribution: $PRETTY_NAME"
# Confirmation prompt
if [ "$NO_CONFIRM" = false ]; then
echo -e "${YELLOW}This script will install and configure:${NC}"
echo " • WireGuard VPN server"
echo " • Pi-hole DNS ad blocker"
echo " • Unbound recursive DNS resolver"
echo ""
read -p "Do you want to continue? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_info "Installation cancelled"
exit 0
fi
fi
# Step 1: Update system packages
echo -e "\n${BLUE}[1/6] Updating system packages...${NC}"
eval "$PKG_UPDATE"
log_success "System packages updated"
# Step 2: Install required packages
echo -e "\n${BLUE}[2/6] Installing required packages...${NC}"
if [ "$PKG_MGR" = "apt" ]; then
$PKG_INSTALL wireguard unbound curl qrencode $IPTABLES_PKG
else
$PKG_INSTALL wireguard-tools unbound curl qrencode $IPTABLES_PKG
systemctl enable iptables
fi
log_success "Required packages installed"
# Step 3: Configure Unbound
echo -e "\n${BLUE}[3/6] Configuring Unbound DNS resolver...${NC}"
# Backup original config
if [ -f /etc/unbound/unbound.conf ]; then
cp /etc/unbound/unbound.conf /etc/unbound/unbound.conf.backup
fi
# Create Unbound configuration
cat > /etc/unbound/unbound.conf << 'EOF'
server:
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip6: no
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
num-threads: 1
so-rcvbuf: 1m
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: fd00::/8
private-address: fe80::/10
EOF
# Set proper permissions
chmod 644 /etc/unbound/unbound.conf
chown root:root /etc/unbound/unbound.conf
# Start and enable Unbound
systemctl enable unbound
systemctl restart unbound
# Verify Unbound is working
sleep 2
if ! systemctl is-active --quiet unbound; then
log_error "Unbound failed to start"
exit 1
fi
# Test DNS resolution
if ! dig @127.0.0.1 -p $UNBOUND_PORT example.com +short >/dev/null 2>&1; then
log_warning "Unbound DNS test failed, but continuing..."
fi
log_success "Unbound configured and running on port $UNBOUND_PORT"
# Step 4: Install Pi-hole
echo -e "\n${BLUE}[4/6] Installing Pi-hole...${NC}"
# Create Pi-hole setup configuration for automated install
cat > /tmp/pihole-setup.conf << EOF
PIHOLE_INTERFACE=$(ip route | grep default | head -n1 | awk '{print $5}')
IPV4_ADDRESS=$(ip route get 1.1.1.1 | grep -oP 'src \K\S+')
IPV6_ADDRESS=
QUERY_LOGGING=true
INSTALL_WEB_SERVER=true
INSTALL_WEB_INTERFACE=true
LIGHTTPD_ENABLED=true
PIHOLE_DNS_1=127.0.0.1#5335
PIHOLE_DNS_2=
DNS_FQDN_REQUIRED=true
DNS_BOGUS_PRIV=true
DNSSEC=false
TEMPERATUREUNIT=C
WEBUIBOXEDLAYOUT=traditional
API_EXCLUDE_DOMAINS=
API_EXCLUDE_CLIENTS=
API_QUERY_LOG_SHOW=permittedonly
API_PRIVACY_MODE=false
EOF
# Install Pi-hole with automated configuration
curl -sSL https://install.pi-hole.net | bash /dev/stdin --unattended
log_success "Pi-hole installed"
# Step 5: Configure Pi-hole DNS settings
echo -e "\n${BLUE}[5/6] Configuring Pi-hole DNS settings...${NC}"
# Set Pi-hole to use Unbound
pihole -a setdns 127.0.0.1#5335
# Restart Pi-hole DNS service
pihole restartdns
# Verify Pi-hole is working
sleep 2
if ! systemctl is-active --quiet pihole-FTL; then
log_error "Pi-hole failed to start"
exit 1
fi
SERVER_IP=$(hostname -I | awk '{print $1}')
log_success "Pi-hole configured. Web interface available at: http://${SERVER_IP}/admin/"
# Step 6: Configure firewall and show WireGuard setup info
echo -e "\n${BLUE}[6/6] Finalizing configuration...${NC}"
# Configure firewall rules for different distributions
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then
ufw allow 51820/udp comment "WireGuard"
ufw allow 53/udp comment "DNS"
ufw allow 80/tcp comment "Pi-hole web"
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=51820/udp --zone=public
firewall-cmd --permanent --add-port=53/udp --zone=public
firewall-cmd --permanent --add-port=80/tcp --zone=public
firewall-cmd --reload
fi
# Generate WireGuard server keys if they don't exist
WG_DIR="/etc/wireguard"
mkdir -p "$WG_DIR"
chmod 755 "$WG_DIR"
if [ ! -f "$WG_DIR/server_private.key" ]; then
wg genkey | tee "$WG_DIR/server_private.key" | wg pubkey > "$WG_DIR/server_public.key"
chmod 600 "$WG_DIR/server_private.key"
chmod 644 "$WG_DIR/server_public.key"
fi
log_success "Installation completed successfully!"
# Final verification
echo -e "\n${GREEN}=== Verification ===${NC}"
echo "✓ Unbound status: $(systemctl is-active unbound)"
echo "✓ Pi-hole status: $(systemctl is-active pihole-FTL)"
echo "✓ WireGuard tools: $(which wg)"
echo -e "\n${GREEN}=== Next Steps ===${NC}"
echo "1. Access Pi-hole web interface: http://${SERVER_IP}/admin/"
echo "2. Configure WireGuard server at: $WG_DIR/"
echo "3. Add blocklists in Pi-hole admin panel"
echo "4. Create WireGuard client configurations"
echo -e "\n${YELLOW}Note:${NC} Remember to configure your WireGuard server configuration"
echo "to use ${SERVER_IP}:53 as the DNS server for clients."
Review the script before running. Execute with: bash install.sh