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?

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 "$@"