Blocky vs Pi-hole: Key Advantages
- Lightweight - Single Go binary (vs Pi-hole’s PHP/SQLite/dnsmasq stack)
- Qubes-optimized - Native NFTables support & vif* interface handling
- No web UI - Reduced attack surface (Pi-hole’s admin portal is a risk)
- Simpler maintenance - Config = one YAML file (vs Pi-hole’s multiple configs/SQL DB)
- Built for containers - Statically compiled Go binary works better in Qubes VMs
- Native Prometheus - Metrics without add-ons (Pi-hole needs exporters)
Ideal for Qubes because:
- Minimal template bloat
- Secure by design (no unnecessary services)
- Easier to firewall
- Clean integration with Qubes networking
Pi-hole drawbacks in Qubes
- Heavy dependencies (200MB+ footprint)
- Web UI requires opening ports
- dnsmasq often conflicts with Qubes networking
- Complex backup/restore
Blocky delivers equivalent ad-blocking with Qubes-friendly architecture.
Quick Start
> Copy the script and run it from dom0 then:
Set other VMs to use sys-blocky as their NetVM.
Done! Ads/trackers blocked—zero performance overhead.
What does the script do?
- Creates a dedicated Qubes OS VM (sys-blocky) for DNS blocking.
- Sets up a minimal Debian template with Go and core dependencies.
- Compiles and installs Blocky (optimized Go binary) as a systemd service.
- Configures NFTables firewall rules to redirect VM DNS traffic
- Blocks ads/trackers using pre-configured denylists (StevenBlack/hosts etc.).
- Ensures reboot persistence via rc.local.d.
- Provides native Prometheus metrics for monitoring.
- Replaces Pi-hole with lower overhead and tighter Qubes integration.
Result: A lightweight, secure, self-contained DNS server for all Qubes VMs.
#!/bin/bash
set -euo pipefail
TEMPLATE_NAME="debian-12-minimal"
CLONED_TEMPLATE="d12m-blk-template"
VM_NAME="sys-blocky"
NETVM="sys-net"
MEMORY=1000
MAXMEM=2000
VCPUS=2
ADDITIONAL_SIZE="15G"
BLOCKY_REPO="https://github.com/0xERR0R/blocky.git"
BLOCKY_DEST="/opt/blocky"
BLOCKY_BIN="/usr/local/bin/blocky"
GO_VERSION="1.24.2"
LOG_FILE="/var/log/blocky_setup.log"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'
log() {
local level="$1"
local message="$2"
local color
case "$level" in
"INFO") color="${BLUE}[*]${NC}" ;;
"SUCCESS") color="${GREEN}[✓]${NC}" ;;
"WARNING") color="${YELLOW}[!]${NC}" ;;
"ERROR") color="${RED}[✗]${NC}" ;;
*) color="${BLUE}[*]${NC}" ;;
esac
echo -e "${color} ${message}${NC}" | tee -a "$LOG_FILE"
}
check_dependencies() {
log "INFO" "Checking dependencies..."
command -v qvm-clone >/dev/null 2>&1 || {
log "ERROR" "Qubes OS tools not found!"
exit 1
}
log "SUCCESS" "Dependencies verified"
}
prepare_template() {
if ! qvm-ls --raw-list | grep -q "$TEMPLATE_NAME"; then
log "INFO" "Installing base template $TEMPLATE_NAME..."
qvm-template install "$TEMPLATE_NAME" || {
log "ERROR" "Failed to install base template"
exit 1
}
else
log "INFO" "Base template $TEMPLATE_NAME exists. Updating..."
qvm-run -p -u root "$TEMPLATE_NAME" "apt update && apt upgrade -y" || {
log "WARNING" "Base template update failed (continuing)"
}
fi
if ! qvm-ls --raw-list | grep -q "$CLONED_TEMPLATE"; then
log "INFO" "Cloning $TEMPLATE_NAME to $CLONED_TEMPLATE..."
qvm-clone "$TEMPLATE_NAME" "$CLONED_TEMPLATE" || {
log "ERROR" "Template clone failed"
exit 1
}
log "INFO" "Installing essential packages..."
qvm-run -u root "$CLONED_TEMPLATE" "apt update && apt install -y git wget" || {
log "ERROR" "Package installation failed"
exit 1
}
qvm-shutdown --wait "$CLONED_TEMPLATE"
log "SUCCESS" "Template cloned and configured"
else
log "WARNING" "Template $CLONED_TEMPLATE already exists. Checking state..."
if ! qvm-run -p "$CLONED_TEMPLATE" "dpkg -l git wget" >/dev/null 2>&1; then
log "INFO" "Installing packages in existing template..."
qvm-run -u root "$CLONED_TEMPLATE" "apt update && apt install -y git wget" && \
qvm-shutdown --wait "$CLONED_TEMPLATE" || {
log "ERROR" "Failed to update existing template"
exit 1
}
fi
log "SUCCESS" "Using existing cloned template"
fi
}
create_blocky_vm() {
if qvm-ls --raw-list | grep -q "$VM_NAME"; then
log "WARNING" "VM $VM_NAME exists. Removing previous version..."
qvm-remove --force "$VM_NAME" || {
log "ERROR" "Failed to remove existing VM"
exit 1
}
log "SUCCESS" "Old VM removed"
fi
log "INFO" "Creating new VM $VM_NAME..."
qvm-create --standalone -t "$CLONED_TEMPLATE" -l red "$VM_NAME" || {
log "ERROR" "VM creation failed"
exit 1
}
qvm-prefs "$VM_NAME" memory "$MEMORY"
qvm-prefs "$VM_NAME" maxmem "$MAXMEM"
qvm-prefs "$VM_NAME" vcpus "$VCPUS"
qvm-prefs "$VM_NAME" netvm "$NETVM"
qvm-prefs "$VM_NAME" provides_network true
log "SUCCESS" "VM $VM_NAME created and configured"
}
install_components() {
log "INFO" "Installing Go $GO_VERSION..."
qvm-run -p -u root "$VM_NAME" "bash -c '
wget -q https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz -O /tmp/go.tar.gz &&
tar -C /usr/local -xzf /tmp/go.tar.gz &&
echo \"export PATH=\\\$PATH:/usr/local/go/bin\" >> /etc/profile &&
echo \"export PATH=\\\$PATH:/usr/local/go/bin\" >> /home/user/.bashrc &&
rm -f /tmp/go.tar.gz
'" || {
log "ERROR" "Go installation failed"
exit 1
}
log "INFO" "Installing Blocky..."
ARCH=$(qvm-run -p "$VM_NAME" "uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/;s/armv7l/armv7/'") || {
log "ERROR" "Failed to detect architecture"
exit 1
}
qvm-run -p -u root "$VM_NAME" "bash -c '
set -e
rm -rf \"$BLOCKY_DEST\" && git clone --depth 1 \"$BLOCKY_REPO\" \"$BLOCKY_DEST\"
cd \"$BLOCKY_DEST\"
/usr/local/go/bin/go build \
-ldflags=\"-X '\''github.com/0xERR0R/blocky/util.Version=$GO_VERSION'\'' \
-X '\''github.com/0xERR0R/blocky/util.BuildTime=\$(date +%Y-%m-%dT%H:%M:%SZ)'\'' \
-X '\''github.com/0xERR0R/blocky/util.Architecture=$ARCH'\''\" \
-o \"$BLOCKY_BIN\"
'" || {
log "ERROR" "Blocky installation failed"
exit 1
}
log "SUCCESS" "Components installed"
}
configure_services() {
log "INFO" "Creating Blocky config..."
qvm-run -p -u root "$VM_NAME" "bash -c '
mkdir -p /etc/blocky &&
cat > /etc/blocky/config.yml <<\"EOF\"
upstreams:
groups:
default:
- 46.227.67.134 #OVPN
- 192.165.9.158 #OVPN
# - 1.1.1.1
# - 8.8.8.8
ports:
dns: 53
blocking:
denylists:
ads:
- \"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts\"
- \"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt\"
- \"http://sysctl.org/camaleon/hosts\"
- \"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt\"
# custom:
# - file://etc/blocky/local-blacklist.txt
clientGroupsBlock:
default:
- \"ads\"
# - \"custom\"
# blockType: ZeroIp
#prometheus:
# enable: true
#port: 53
#httpPort: 4000
EOF
'" || {
log "ERROR" "Blocky configuration failed"
exit 1
}
log "INFO" "Configuring systemd service..."
qvm-run -p -u root "$VM_NAME" "bash -c '
cat > /etc/systemd/system/blocky.service <<\"EOF\"
[Unit]
Description=Blocky DNS
After=network.target
[Service]
ExecStart=/usr/local/bin/blocky --config /etc/blocky/config.yml
Restart=always
User=root
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload &&
systemctl enable --now blocky
'" || {
log "ERROR" "Service configuration failed"
exit 1
}
log "SUCCESS" "Services configured"
}
setup_persistence() {
log "INFO" "Configuring rc.local.d..."
qvm-run -p -u root "$VM_NAME" "bash -c '
mkdir -p /rw/config/rc.local.d &&
cat > /rw/config/rc.local.d/blocky-start.sh <<\"EOF\"
#!/bin/bash
sudo systemctl unmask blocky.service
sudo systemctl daemon-reload
sudo systemctl enable --now blocky.service
EOF
chmod +x /rw/config/rc.local.d/blocky-start.sh
'" || {
log "ERROR" "rc.local.d setup failed"
exit 1
}
log "INFO" "Configuring rc.local..."
qvm-run -p -u root "$VM_NAME" "bash -c '
echo -e \"#!/bin/bash\\nexec /rw/config/rc.local.d/blocky-start.sh\" > /rw/config/rc.local &&
chmod +x /rw/config/rc.local
'" || {
log "ERROR" "rc.local setup failed"
exit 1
}
log "SUCCESS" "Persistence configured"
}
configure_firewall() {
log "INFO" "Configuring firewall rules..."
qvm-run -p -u root "$VM_NAME" "bash -c '
mkdir -p /rw/config/{network-hooks.d,qubes-firewall.d} &&
cat > /rw/config/network-hooks.d/internalise.sh <<\"EOF\"
#!/bin/sh
find /proc/sys/net/ipv4/conf -name \"vif*\" -exec bash -c \"echo 1 | tee {}/route_localnet\" \;
EOF
cat > /rw/config/network-hooks.d/update_nft.sh <<\"EOF\"
#!/bin/sh
nft -f /rw/config/qubes-firewall.d/update_nft.nft
EOF
cat > /rw/config/qubes-firewall.d/internalise.sh <<\"EOF\"
#!/bin/sh
find /proc/sys/net/ipv4/conf -name \"vif*\" -exec bash -c \"echo 1 | tee {}/route_localnet\" \;
EOF
cat > /rw/config/qubes-firewall.d/update_nft.sh <<\"EOF\"
#!/bin/sh
nft -f /rw/config/qubes-firewall.d/update_nft.nft
EOF
cat > /rw/config/qubes-firewall.d/update_nft.nft <<\"EOF\"
#!/usr/sbin/nft -f
flush chain qubes dnat-dns
flush chain qubes custom-forward
insert rule qubes custom-forward tcp dport 53 drop
insert rule qubes custom-forward udp dport 53 drop
flush chain qubes custom-input
insert rule qubes custom-input tcp dport 53 accept
insert rule qubes custom-input udp dport 53 accept
flush chain qubes dnat-dns
insert rule qubes dnat-dns iifname \"vif*\" tcp dport 53 dnat to 127.0.0.1
insert rule qubes dnat-dns iifname \"vif*\" udp dport 53 dnat to 127.0.0.1
EOF
chmod +x /rw/config/rc.local \
/rw/config/qubes-firewall.d/* \
/rw/config/network-hooks.d/*
'" || {
log "ERROR" "Firewall configuration failed"
exit 1
}
log "SUCCESS" "Firewall configured"
}
finalize_setup() {
log "INFO" "Disabling unnecessary services..."
qvm-features "$VM_NAME" service.cups 0
qvm-features "$VM_NAME" service.cups-browsed 0
log "INFO" "Expanding storage..."
qvm-shutdown --wait "$VM_NAME"
qvm-start "$VM_NAME"
log "INFO" "Verifying installation..."
qvm-run -p -u root "$VM_NAME" "systemctl status blocky"
qvm-run -p -u root "$VM_NAME" "blocky version"
log "SUCCESS" "Setup complete!"
echo -e "${CYAN}╔═════════════════════════════════════╗"
echo -e "║ BLOCKY INSTALLATION COMPLETE! ║"
echo -e "╠═════════════════════════════════════╣"
echo -e "║ • Use sys-blocky as NetVM ║"
echo -e "║ • Live journal started ║"
echo -e "╚═════════════════════════════════════╝${NC}"
log "INFO" "Starting live journal..."
qvm-run -p -u root "$VM_NAME" "xterm -T 'BLOCKY LIVE JOURNAL' -e 'journalctl -u blocky -f'"
}
main() {
check_dependencies
prepare_template
create_blocky_vm
install_components
configure_services
setup_persistence
configure_firewall
finalize_setup
}
main "$@"