#!/bin/bash # ============================================================================= # Pi-hole Baremetal Installer # ============================================================================= # Network-wide ad blocking DNS server - installs directly on the system # # Supported: Ubuntu, Debian, Fedora, Rocky/Alma/RHEL 8+, Arch, openSUSE, FreeBSD # Works on minimal/headless installs # # Usage: # curl -fsSL /install.sh | sudo bash # # Options: # --unattended Skip all prompts, use defaults # --no-lighttpd Skip web server (API/admin only via CLI) # --set-password Set admin password non-interactively (reads from stdin) # --ipv4 Set static IPv4 address # --interface Set network interface (e.g., eth0) # --dns1 Set upstream DNS 1 (default: 1.1.1.1) # --dns2 Set upstream DNS 2 (default: 1.0.0.1) # ============================================================================= set -o pipefail # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' log() { echo -e "${BLUE}[INFO]${NC} $1"; } success() { echo -e "${GREEN}[OK]${NC} $1"; } warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } # Configuration defaults UNATTENDED=false INSTALL_LIGHTTPD=true PIHOLE_DNS_1="1.1.1.1" PIHOLE_DNS_2="1.0.0.1" INTERFACE="" IPV4_ADDRESS="" ADMIN_PASSWORD="" INSTALL_DIR="/etc/pihole" PIHOLE_SKIP_OS_CHECK=false # Parse arguments while [ $# -gt 0 ]; do case $1 in --unattended) UNATTENDED=true; shift ;; --no-lighttpd) INSTALL_LIGHTTPD=false; shift ;; --set-password) ADMIN_PASSWORD="$2"; shift 2 ;; --ipv4) IPV4_ADDRESS="$2"; shift 2 ;; --interface) INTERFACE="$2"; shift 2 ;; --dns1) PIHOLE_DNS_1="$2"; shift 2 ;; --dns2) PIHOLE_DNS_2="$2"; shift 2 ;; --skip-os-check) PIHOLE_SKIP_OS_CHECK=true; shift ;; --help|-h) echo "Pi-hole Baremetal Installer" echo "" echo "Usage: install.sh [options]" echo "" echo "Options:" echo " --unattended Skip prompts, use defaults" echo " --no-lighttpd Skip web server installation" echo " --set-password X Set admin password" echo " --ipv4 Set static IPv4 (e.g., 192.168.1.10/24)" echo " --interface Set interface (e.g., eth0)" echo " --dns1 Upstream DNS 1 (default: 1.1.1.1)" echo " --dns2 Upstream DNS 2 (default: 1.0.0.1)" echo " --skip-os-check Skip OS compatibility check" exit 0 ;; *) shift ;; esac done # Check root [ "$(id -u)" -ne 0 ] && error "Run as root: sudo bash install.sh" # Detect OS detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID OS_VERSION=${VERSION_ID:-} OS_NAME="${NAME:-$OS}" elif [ "$(uname)" = "FreeBSD" ]; then OS="freebsd" OS_VERSION=$(freebsd-version -u 2>/dev/null | cut -d'-' -f1) OS_NAME="FreeBSD" else error "Cannot detect OS" fi log "Detected: $OS_NAME $OS_VERSION" } # Check if OS is supported by official Pi-hole installer check_pihole_support() { case $OS in ubuntu|debian|raspbian) # Supported versions return 0 ;; fedora) return 0 ;; centos|rhel|rocky|almalinux) return 0 ;; *) if [ "$PIHOLE_SKIP_OS_CHECK" = true ]; then warn "OS '$OS' may not be officially supported by Pi-hole" warn "Attempting installation anyway (--skip-os-check enabled)" return 0 else return 1 fi ;; esac } # Wait for apt/dpkg locks wait_for_apt_lock() { local max_wait=120 local waited=0 while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 || \ fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || \ fuser /var/lib/dpkg/lock >/dev/null 2>&1; do if [ $waited -eq 0 ]; then log "Waiting for apt/dpkg lock to be released..." fi sleep 5 waited=$((waited + 5)) if [ $waited -ge $max_wait ]; then warn "Timeout waiting for apt lock, attempting to continue..." break fi done } # Wait for zypper lock wait_for_zypper_lock() { local max_wait=120 local waited=0 while pgrep -x zypper >/dev/null 2>&1; do if [ $waited -eq 0 ]; then log "Waiting for zypper lock to be released..." fi sleep 5 waited=$((waited + 5)) if [ $waited -ge $max_wait ]; then warn "Timeout waiting for zypper lock..." break fi done } # Install prerequisites install_prerequisites() { log "Installing prerequisites..." case $OS in ubuntu|debian|raspbian|linuxmint|pop) export DEBIAN_FRONTEND=noninteractive wait_for_apt_lock apt-get update -qq apt-get install -y -qq curl ca-certificates git iproute2 procps >/dev/null 2>&1 ;; fedora) dnf install -y -q curl ca-certificates git iproute procps-ng >/dev/null 2>&1 ;; rocky|almalinux|rhel|centos) if command -v dnf >/dev/null 2>&1; then dnf install -y -q curl ca-certificates git iproute procps-ng >/dev/null 2>&1 else yum install -y curl ca-certificates git iproute procps >/dev/null 2>&1 fi ;; arch|manjaro|endeavouros) pacman -Sy --noconfirm --quiet curl ca-certificates git iproute2 procps-ng >/dev/null 2>&1 ;; opensuse*|sles) wait_for_zypper_lock zypper install -y curl ca-certificates git iproute2 procps >/dev/null 2>&1 ;; freebsd) env ASSUME_ALWAYS_YES=YES pkg bootstrap >/dev/null 2>&1 || true pkg install -y bash curl git ca_root_nss >/dev/null 2>&1 ;; *) warn "Unknown OS, attempting to continue..." ;; esac success "Prerequisites installed" } # Detect network interface detect_interface() { if [ -n "$INTERFACE" ]; then return fi # Try to find the default interface INTERFACE=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'dev \K\S+' | head -1) if [ -z "$INTERFACE" ]; then # Fallback: get first non-loopback interface INTERFACE=$(ip -o link show | awk -F': ' '$2 != "lo" {print $2; exit}') fi if [ -z "$INTERFACE" ]; then INTERFACE="eth0" fi log "Using interface: $INTERFACE" } # Detect IP address detect_ip() { if [ -n "$IPV4_ADDRESS" ]; then return fi # Get current IP on the detected interface IPV4_ADDRESS=$(ip -4 addr show "$INTERFACE" 2>/dev/null | grep -oP 'inet \K[\d.]+/\d+' | head -1) if [ -z "$IPV4_ADDRESS" ]; then # Fallback: get any non-loopback IP IPV4_ADDRESS=$(hostname -I 2>/dev/null | awk '{print $1}') if [ -n "$IPV4_ADDRESS" ]; then IPV4_ADDRESS="${IPV4_ADDRESS}/24" fi fi if [ -z "$IPV4_ADDRESS" ]; then error "Could not detect IP address. Use --ipv4 to specify." fi log "Using IP: $IPV4_ADDRESS" } # Get gateway detect_gateway() { GATEWAY=$(ip route | grep default | awk '{print $3}' | head -1) if [ -z "$GATEWAY" ]; then GATEWAY="192.168.1.1" fi log "Gateway: $GATEWAY" } # Create Pi-hole setup vars for unattended install create_setupvars() { log "Creating Pi-hole configuration..." mkdir -p "$INSTALL_DIR" # Extract IP without CIDR for some settings IP_ONLY=$(echo "$IPV4_ADDRESS" | cut -d'/' -f1) cat > "$INSTALL_DIR/setupVars.conf" << EOF PIHOLE_INTERFACE=$INTERFACE IPV4_ADDRESS=$IPV4_ADDRESS IPV6_ADDRESS= PIHOLE_DNS_1=$PIHOLE_DNS_1 PIHOLE_DNS_2=$PIHOLE_DNS_2 QUERY_LOGGING=true INSTALL_WEB_SERVER=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" ) INSTALL_WEB_INTERFACE=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" ) LIGHTTPD_ENABLED=$( [ "$INSTALL_LIGHTTPD" = true ] && echo "true" || echo "false" ) CACHE_SIZE=10000 DNS_FQDN_REQUIRED=true DNS_BOGUS_PRIV=true DNSMASQ_LISTENING=local WEBPASSWORD= BLOCKING_ENABLED=true EOF success "Configuration created at $INSTALL_DIR/setupVars.conf" } # Install Pi-hole using official installer install_pihole_official() { log "Installing Pi-hole (official installer)..." # Download and run official installer if [ "$UNATTENDED" = true ]; then curl -sSL https://install.pi-hole.net | bash /dev/stdin --unattended else curl -sSL https://install.pi-hole.net | bash fi if [ $? -ne 0 ]; then error "Pi-hole installation failed" fi success "Pi-hole installed successfully" } # Install Pi-hole on Arch Linux (manual process) install_pihole_arch() { log "Installing Pi-hole on Arch Linux..." # Install dependencies pacman -Sy --noconfirm --needed \ php php-cgi php-sqlite php-intl \ lighttpd \ base-devel git \ dnsmasq \ sudo \ inetutils >/dev/null 2>&1 # Clone Pi-hole from AUR or use manual install if command -v yay >/dev/null 2>&1; then log "Using yay to install Pi-hole from AUR..." sudo -u nobody yay -S --noconfirm pi-hole-server pi-hole-ftl 2>/dev/null || true elif command -v paru >/dev/null 2>&1; then log "Using paru to install Pi-hole from AUR..." sudo -u nobody paru -S --noconfirm pi-hole-server pi-hole-ftl 2>/dev/null || true fi # If AUR helpers failed or not available, try official installer with skip if ! command -v pihole >/dev/null 2>&1; then warn "AUR installation failed, trying official installer..." export PIHOLE_SKIP_OS_CHECK=true curl -sSL https://install.pi-hole.net | PIHOLE_SKIP_OS_CHECK=true bash /dev/stdin --unattended fi success "Pi-hole installed on Arch Linux" } # Install Pi-hole on openSUSE install_pihole_opensuse() { log "Installing Pi-hole on openSUSE..." wait_for_zypper_lock # Install dependencies zypper install -y \ php8 php8-cgi php8-sqlite php8-intl \ lighttpd \ git curl \ dnsmasq \ iproute2 >/dev/null 2>&1 # Try official installer with skip export PIHOLE_SKIP_OS_CHECK=true curl -sSL https://install.pi-hole.net | PIHOLE_SKIP_OS_CHECK=true bash /dev/stdin --unattended success "Pi-hole installed on openSUSE" } # Install Pi-hole on FreeBSD install_pihole_freebsd() { log "Installing Pi-hole on FreeBSD..." echo "" echo "========================================" echo " FreeBSD Pi-hole Installation" echo "========================================" echo "" echo "FreeBSD is not officially supported by Pi-hole." echo "" echo "Alternatives for FreeBSD:" echo "" echo "1. Use AdGuard Home (better FreeBSD support):" echo " pkg install adguardhome" echo " sysrc adguardhome_enable=YES" echo " service adguardhome start" echo " # Access at http://localhost:3000" echo "" echo "2. Use Unbound + blocklists manually" echo "" echo "3. Run Pi-hole in a Linux jail (bhyve/vm)" echo "" # Offer to install AdGuard Home instead if [ "$UNATTENDED" != true ]; then read -p "Install AdGuard Home instead? [Y/n] " -n 1 -r echo if [[ ! $REPLY =~ ^[Nn]$ ]]; then pkg install -y adguardhome sysrc adguardhome_enable=YES service adguardhome start success "AdGuard Home installed! Access at http://localhost:3000" exit 0 fi fi error "FreeBSD is not supported for Pi-hole baremetal installation" } # Configure firewall configure_firewall() { log "Configuring firewall..." if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld 2>/dev/null; then firewall-cmd --permanent --add-service=dns 2>/dev/null || true firewall-cmd --permanent --add-service=http 2>/dev/null || true firewall-cmd --permanent --add-service=https 2>/dev/null || true firewall-cmd --permanent --add-port=53/tcp 2>/dev/null || true firewall-cmd --permanent --add-port=53/udp 2>/dev/null || true firewall-cmd --permanent --add-port=80/tcp 2>/dev/null || true firewall-cmd --permanent --add-port=4711/tcp 2>/dev/null || true # FTL firewall-cmd --reload 2>/dev/null || true success "Firewall configured (firewalld)" elif command -v ufw >/dev/null 2>&1 && ufw status | grep -q "active"; then ufw allow 53/tcp 2>/dev/null || true ufw allow 53/udp 2>/dev/null || true ufw allow 80/tcp 2>/dev/null || true ufw allow 4711/tcp 2>/dev/null || true success "Firewall configured (ufw)" elif command -v iptables >/dev/null 2>&1; then # Basic iptables rules iptables -A INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || true iptables -A INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || true iptables -A INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || true iptables -A INPUT -p tcp --dport 4711 -j ACCEPT 2>/dev/null || true success "Firewall rules added (iptables)" else warn "No firewall detected, skipping configuration" fi } # Set admin password set_admin_password() { if [ -n "$ADMIN_PASSWORD" ]; then log "Setting admin password..." pihole -a -p "$ADMIN_PASSWORD" success "Admin password set" elif [ "$UNATTENDED" != true ]; then echo "" echo "Set your Pi-hole admin password:" pihole -a -p fi } # Create management script create_management_script() { log "Creating management script..." cat > /usr/local/bin/pihole-manage << 'EOFSCRIPT' #!/bin/bash # Pi-hole management helper case "${1:-help}" in status) echo "=== Pi-hole Status ===" pihole status echo "" echo "=== Service Status ===" systemctl status pihole-FTL --no-pager 2>/dev/null || service pihole-FTL status ;; logs) tail -f /var/log/pihole/pihole.log ;; ftl-logs) tail -f /var/log/pihole/FTL.log ;; query) shift pihole -q "$@" ;; update) pihole -up ;; restart) pihole restartdns ;; disable) duration="${2:-}" if [ -n "$duration" ]; then pihole disable "$duration" else pihole disable fi ;; enable) pihole enable ;; stats) pihole -c -e ;; top-ads) echo "=== Top Blocked Domains ===" pihole -t ;; top-clients) sqlite3 /etc/pihole/pihole-FTL.db \ "SELECT client, COUNT(*) as count FROM queries GROUP BY client ORDER BY count DESC LIMIT 10;" 2>/dev/null || \ echo "Database query not available" ;; whitelist) shift pihole -w "$@" ;; blacklist) shift pihole -b "$@" ;; gravity) pihole -g ;; backup) timestamp=$(date +"%Y%m%d_%H%M%S") backup_dir="/var/backups/pihole" mkdir -p "$backup_dir" pihole -a -t "$backup_dir/pihole_backup_$timestamp.tar.gz" echo "Backup created: $backup_dir/pihole_backup_$timestamp.tar.gz" ;; password) pihole -a -p ;; *) echo "Pi-hole Management Helper" echo "" echo "Usage: pihole-manage " echo "" echo "Commands:" echo " status Show Pi-hole and service status" echo " logs Tail the Pi-hole log" echo " ftl-logs Tail the FTL log" echo " query Query the log for domain " echo " update Update Pi-hole" echo " restart Restart DNS resolver" echo " disable [t] Disable blocking (optionally for t seconds)" echo " enable Enable blocking" echo " stats Show statistics" echo " top-ads Show top blocked domains" echo " whitelist Add domain to whitelist" echo " blacklist Add domain to blacklist" echo " gravity Update blocklists" echo " backup Create configuration backup" echo " password Change admin password" echo "" echo "Pi-hole native commands: pihole -h" ;; esac EOFSCRIPT chmod +x /usr/local/bin/pihole-manage success "Management script created: pihole-manage" } # Show completion message show_complete() { local IP=$(echo "$IPV4_ADDRESS" | cut -d'/' -f1) if [ -z "$IP" ]; then IP=$(hostname -I 2>/dev/null | awk '{print $1}') fi echo "" echo "========================================" echo " Pi-hole Installation Complete!" echo "========================================" echo "" echo "Access:" echo " Admin Panel: http://$IP/admin" echo " DNS Server: $IP" echo "" echo "Configuration:" echo " Interface: $INTERFACE" echo " IPv4: $IPV4_ADDRESS" echo " Upstream DNS: $PIHOLE_DNS_1, $PIHOLE_DNS_2" echo "" echo "Commands:" echo " pihole status - Check status" echo " pihole -c - Console dashboard" echo " pihole -up - Update Pi-hole" echo " pihole -g - Update gravity (blocklists)" echo " pihole-manage help - Management helper" echo "" echo "Client Configuration:" echo " Set your devices/router DNS to: $IP" echo "" if [ -z "$ADMIN_PASSWORD" ] && [ "$UNATTENDED" = true ]; then echo "⚠️ Set admin password:" echo " pihole -a -p" echo "" fi echo "Logs: /var/log/pihole/" echo "Config: /etc/pihole/" echo "" } # Main installation main() { echo "" echo "========================================" echo " Pi-hole Baremetal Installer" echo "========================================" echo "" detect_os install_prerequisites detect_interface detect_ip detect_gateway # Create config for unattended install if [ "$UNATTENDED" = true ]; then create_setupvars fi # Route to appropriate installer case $OS in ubuntu|debian|raspbian|fedora|centos|rhel|rocky|almalinux) install_pihole_official ;; arch|manjaro|endeavouros) install_pihole_arch ;; opensuse*|sles) install_pihole_opensuse ;; freebsd) install_pihole_freebsd ;; linuxmint|pop) # Mint and Pop are Ubuntu-based, should work install_pihole_official ;; *) if [ "$PIHOLE_SKIP_OS_CHECK" = true ]; then warn "Attempting installation on unsupported OS: $OS" install_pihole_official else error "Unsupported OS: $OS. Use --skip-os-check to force installation." fi ;; esac configure_firewall set_admin_password create_management_script show_complete } main "$@"