#!/usr/bin/env bash # shellcheck disable=SC1090 # Pi-hole: A black hole for Internet advertisements # (c) 2017-2018 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # # Installs and Updates Pi-hole # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. # pi-hole.net/donate # # Install with this command (from your Linux machine): # # curl -sSL https://install.pi-hole.net | bash # -e option instructs bash to immediately exit if any command [1] has a non-zero exit status # We do not want users to end up with a partially working install, so we exit the script # instead of continuing the installation with something broken set -e ######## VARIABLES ######### # For better maintainability, we store as much information that can change in variables # This allows us to make a change in one place that can propagate to all instances of the variable # These variables should all be GLOBAL variables, written in CAPS # Local variables will be in lowercase and will exist only within functions # It's still a work in progress, so you may see some variance in this guideline until it is complete # List of supported DNS servers DNS_SERVERS=$(cat << EOM Google (ECS);8.8.8.8;8.8.4.4;2001:4860:4860:0:0:0:0:8888;2001:4860:4860:0:0:0:0:8844 OpenDNS (ECS);208.67.222.222;208.67.220.220;2620:0:ccc::2;2620:0:ccd::2 Level3;4.2.2.1;4.2.2.2;; Comodo;8.26.56.26;8.20.247.20;; DNS.WATCH;84.200.69.80;84.200.70.40;2001:1608:10:25:0:0:1c04:b12f;2001:1608:10:25:0:0:9249:d69b Quad9 (filtered, DNSSEC);9.9.9.9;149.112.112.112;2620:fe::fe;2620:fe::9 Quad9 (unfiltered, no DNSSEC);9.9.9.10;149.112.112.10;2620:fe::10;2620:fe::fe:10 Quad9 (filtered + ECS);9.9.9.11;149.112.112.11;2620:fe::11; Cloudflare;1.1.1.1;1.0.0.1;2606:4700:4700::1111;2606:4700:4700::1001 EOM ) # Location for final installation log storage installLogLoc=/etc/pihole/install.log # This is an important file as it contains information specific to the machine it's being installed on setupVars=/etc/pihole/setupVars.conf # Pi-hole uses lighttpd as a Web server, and this is the config file for it # shellcheck disable=SC2034 lighttpdConfig=/etc/lighttpd/lighttpd.conf # This is a file used for the colorized output coltable=/opt/pihole/COL_TABLE # Root of the web server webroot="/var/www/html" # We store several other directories and webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git" webInterfaceDir="${webroot}/admin" piholeGitUrl="https://github.com/pi-hole/pi-hole.git" PI_HOLE_LOCAL_REPO="/etc/.pihole" # These are the names of pi-holes files, stored in an array PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage) # This directory is where the Pi-hole scripts will be installed PI_HOLE_INSTALL_DIR="/opt/pihole" PI_HOLE_CONFIG_DIR="/etc/pihole" PI_HOLE_BLOCKPAGE_DIR="${webroot}/pihole" useUpdateVars=false adlistFile="/etc/pihole/adlists.list" regexFile="/etc/pihole/regex.list" # Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until # this script can run IPV4_ADDRESS="" IPV6_ADDRESS="" # By default, query logging is enabled and the dashboard is set to be installed QUERY_LOGGING=true INSTALL_WEB_INTERFACE=true PRIVACY_LEVEL=0 if [ -z "${USER}" ]; then USER="$(id -un)" fi # Find the rows and columns will default to 80x24 if it can not be detected screen_size=$(stty size || printf '%d %d' 24 80) # Set rows variable to contain first number printf -v rows '%d' "${screen_size%% *}" # Set columns variable to contain second number printf -v columns '%d' "${screen_size##* }" # Divide by two so the dialogs take up half of the screen, which looks nice. r=$(( rows / 2 )) c=$(( columns / 2 )) # Unless the screen is tiny r=$(( r < 20 ? 20 : r )) c=$(( c < 70 ? 70 : c )) ######## Undocumented Flags. Shhh ######## # These are undocumented flags; some of which we can use when repairing an installation # The runUnattended flag is one example of this skipSpaceCheck=false reconfigure=false runUnattended=false INSTALL_WEB_SERVER=true # Check arguments for the undocumented flags for var in "$@"; do case "$var" in "--reconfigure" ) reconfigure=true;; "--i_do_not_follow_recommendations" ) skipSpaceCheck=true;; "--unattended" ) runUnattended=true;; "--disable-install-webserver" ) INSTALL_WEB_SERVER=false;; esac done # If the color table file exists, if [[ -f "${coltable}" ]]; then # source it source ${coltable} # Otherwise, else # Set these values so the installer can still run in color COL_NC='\e[0m' # No Color COL_LIGHT_GREEN='\e[1;32m' COL_LIGHT_RED='\e[1;31m' TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]" CROSS="[${COL_LIGHT_RED}✗${COL_NC}]" INFO="[i]" # shellcheck disable=SC2034 DONE="${COL_LIGHT_GREEN} done!${COL_NC}" OVER="\\r\\033[K" fi # Define global binary variable binary="tbd" # A simple function that just echoes out our logo in ASCII format # This lets users know that it is a Pi-hole, LLC product show_ascii_berry() { echo -e " ${COL_LIGHT_GREEN}.;;,. .ccccc:,. :cccclll:. ..,, :ccccclll. ;ooodc 'ccll:;ll .oooodc .;cll.;;looo:. ${COL_LIGHT_RED}.. ','. .',,,,,,'. .',,,,,,,,,,. .',,,,,,,,,,,,.... ....''',,,,,,,'....... ......... .... ......... .......... .......... .......... .......... ......... .... ......... ........,,,,,,,'...... ....',,,,,,,,,,,,. .',,,,,,,,,'. .',,,,,,'. ..'''.${COL_NC} " } is_command() { # Checks for existence of string passed in as only function argument. # Exit value of 0 when exists, 1 if not exists. Value is the result # of the `command` shell built-in call. local check_command="$1" command -v "${check_command}" >/dev/null 2>&1 } # Compatibility distro_check() { # If apt-get is installed, then we know it's part of the Debian family if is_command apt-get ; then # Set some global variables here # We don't set them earlier since the family might be Red Hat, so these values would be different PKG_MANAGER="apt-get" # A variable to store the command used to update the package cache UPDATE_PKG_CACHE="${PKG_MANAGER} update" # An array for something... PKG_INSTALL=(${PKG_MANAGER} --yes --no-install-recommends install) # grep -c will return 1 retVal on 0 matches, block this throwing the set -e with an OR TRUE PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true" # Some distros vary slightly so these fixes for dependencies may apply # on Ubuntu 18.04.1 LTS we need to add the universe repository to gain access to dialog and dhcpcd5 APT_SOURCES="/etc/apt/sources.list" if awk 'BEGIN{a=1;b=0}/bionic main/{a=0}/bionic.*universe/{b=1}END{exit a + b}' ${APT_SOURCES}; then if ! whiptail --defaultno --title "Dependencies Require Update to Allowed Repositories" --yesno "Would you like to enable 'universe' repository?\\n\\nThis repository is required by the following packages:\\n\\n- dhcpcd5\\n- dialog" ${r} ${c}; then printf " %b Aborting installation: dependencies could not be installed.\\n" "${CROSS}" exit # exit the installer else printf " %b Enabling universe package repository for Ubuntu Bionic\\n" "${INFO}" cp ${APT_SOURCES} ${APT_SOURCES}.backup # Backup current repo list printf " %b Backed up current configuration to %s\\n" "${TICK}" "${APT_SOURCES}.backup" add-apt-repository universe printf " %b Enabled %s\\n" "${TICK}" "'universe' repository" fi fi # Debian 7 doesn't have iproute2 so if the dry run install is successful, if ${PKG_MANAGER} install --dry-run iproute2 > /dev/null 2>&1; then # we can install it iproute_pkg="iproute2" # Otherwise, else # use iproute iproute_pkg="iproute" fi # Check for and determine version number (major and minor) of current php install if is_command php ; then printf " %b Existing PHP installation detected : PHP version %s\\n" "${INFO}" "$(php <<< "")" printf -v phpInsMajor "%d" "$(php <<< "")" printf -v phpInsMinor "%d" "$(php <<< "")" # Is installed php version 7.0 or greater if [ "${phpInsMajor}" -ge 7 ]; then phpInsNewer=true fi fi # Check if installed php is v 7.0, or newer to determine packages to install if [[ "$phpInsNewer" != true ]]; then # Prefer the php metapackage if it's there if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then phpVer="php" # fall back on the php5 packages else phpVer="php5" fi else # Newer php is installed, its common, cgi & sqlite counterparts are deps phpVer="php$phpInsMajor.$phpInsMinor" fi # We also need the correct version for `php-sqlite` (which differs across distros) if ${PKG_MANAGER} install --dry-run ${phpVer}-sqlite3 > /dev/null 2>&1; then phpSqlite="sqlite3" else phpSqlite="sqlite" fi # Since our install script is so large, we need several other programs to successfully get a machine provisioned # These programs are stored in an array so they can be looped through later INSTALLER_DEPS=(apt-utils dialog debconf dhcpcd5 git ${iproute_pkg} whiptail) # Pi-hole itself has several dependencies that also need to be installed PIHOLE_DEPS=(cron curl dnsutils iputils-ping lsof netcat psmisc sudo unzip wget idn2 sqlite3 libcap2-bin dns-root-data resolvconf libcap2) # The Web dashboard has some that also need to be installed # It's useful to separate the two since our repos are also setup as "Core" code and "Web" code PIHOLE_WEB_DEPS=(lighttpd ${phpVer}-common ${phpVer}-cgi ${phpVer}-${phpSqlite}) # The Web server user, LIGHTTPD_USER="www-data" # group, LIGHTTPD_GROUP="www-data" # and config file LIGHTTPD_CFG="lighttpd.conf.debian" # A function to check... test_dpkg_lock() { # An iterator used for counting loop iterations i=0 # fuser is a program to show which processes use the named files, sockets, or filesystems # So while the command is true while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do # Wait half a second sleep 0.5 # and increase the iterator ((i=i+1)) done # Always return success, since we only return if there is no # lock (anymore) return 0 } # If apt-get is not found, check for rpm to see if it's a Red Hat family OS elif is_command rpm ; then # Then check if dnf or yum is the package manager if is_command dnf ; then PKG_MANAGER="dnf" else PKG_MANAGER="yum" fi # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update. UPDATE_PKG_CACHE=":" PKG_INSTALL=(${PKG_MANAGER} install -y) PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l" INSTALLER_DEPS=(dialog git iproute newt procps-ng which) PIHOLE_DEPS=(bind-utils cronie curl findutils nmap-ncat sudo unzip wget libidn2 psmisc sqlite libcap) PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php-common php-cli php-pdo) LIGHTTPD_USER="lighttpd" LIGHTTPD_GROUP="lighttpd" LIGHTTPD_CFG="lighttpd.conf.fedora" # If the host OS is Fedora, if grep -qiE 'fedora|fedberry' /etc/redhat-release; then # all required packages should be available by default with the latest fedora release # ensure 'php-json' is installed on Fedora (installed as dependency on CentOS7 + Remi repository) PIHOLE_WEB_DEPS+=('php-json') # or if host OS is CentOS, elif grep -qiE 'centos|scientific' /etc/redhat-release; then # Pi-Hole currently supports CentOS 7+ with PHP7+ SUPPORTED_CENTOS_VERSION=7 SUPPORTED_CENTOS_PHP_VERSION=7 # Check current CentOS major release version CURRENT_CENTOS_VERSION=$(grep -oP '(?<= )[0-9]+(?=\.)' /etc/redhat-release) # Check if CentOS version is supported if [[ $CURRENT_CENTOS_VERSION -lt $SUPPORTED_CENTOS_VERSION ]]; then printf " %b CentOS %s is not supported.\\n" "${CROSS}" "${CURRENT_CENTOS_VERSION}" printf " Please update to CentOS release %s or later.\\n" "${SUPPORTED_CENTOS_VERSION}" # exit the installer exit fi # on CentOS we need to add the EPEL repository to gain access to Fedora packages EPEL_PKG="epel-release" rpm -q ${EPEL_PKG} &> /dev/null || rc=$? if [[ $rc -ne 0 ]]; then printf " %b Enabling EPEL package repository (https://fedoraproject.org/wiki/EPEL)\\n" "${INFO}" "${PKG_INSTALL[@]}" ${EPEL_PKG} &> /dev/null printf " %b Installed %s\\n" "${TICK}" "${EPEL_PKG}" fi # The default php on CentOS 7.x is 5.4 which is EOL # Check if the version of PHP available via installed repositories is >= to PHP 7 AVAILABLE_PHP_VERSION=$(${PKG_MANAGER} info php | grep -i version | grep -o '[0-9]\+' | head -1) if [[ $AVAILABLE_PHP_VERSION -ge $SUPPORTED_CENTOS_PHP_VERSION ]]; then # Since PHP 7 is available by default, install via default PHP package names : # do nothing as PHP is current else REMI_PKG="remi-release" REMI_REPO="remi-php72" rpm -q ${REMI_PKG} &> /dev/null || rc=$? if [[ $rc -ne 0 ]]; then # The PHP version available via default repositories is older than version 7 if ! whiptail --defaultno --title "PHP 7 Update (recommended)" --yesno "PHP 7.x is recommended for both security and language features.\\nWould you like to install PHP7 via Remi's RPM repository?\\n\\nSee: https://rpms.remirepo.net for more information" ${r} ${c}; then # User decided to NOT update PHP from REMI, attempt to install the default available PHP version printf " %b User opt-out of PHP 7 upgrade on CentOS. Deprecated PHP may be in use.\\n" "${INFO}" : # continue with unsupported php version else printf " %b Enabling Remi's RPM repository (https://rpms.remirepo.net)\\n" "${INFO}" "${PKG_INSTALL[@]}" "https://rpms.remirepo.net/enterprise/${REMI_PKG}-$(rpm -E '%{rhel}').rpm" &> /dev/null # enable the PHP 7 repository via yum-config-manager (provided by yum-utils) "${PKG_INSTALL[@]}" "yum-utils" &> /dev/null yum-config-manager --enable ${REMI_REPO} &> /dev/null printf " %b Remi's RPM repository has been enabled for PHP7\\n" "${TICK}" # trigger an install/update of PHP to ensure previous version of PHP is updated from REMI if "${PKG_INSTALL[@]}" "php-cli" &> /dev/null; then printf " %b PHP7 installed/updated via Remi's RPM repository\\n" "${TICK}" else printf " %b There was a problem updating to PHP7 via Remi's RPM repository\\n" "${CROSS}" exit 1 fi fi fi fi else # Warn user of unsupported version of Fedora or CentOS if ! whiptail --defaultno --title "Unsupported RPM based distribution" --yesno "Would you like to continue installation on an unsupported RPM based distribution?\\n\\nPlease ensure the following packages have been installed manually:\\n\\n- lighttpd\\n- lighttpd-fastcgi\\n- PHP version 7+" ${r} ${c}; then printf " %b Aborting installation due to unsupported RPM based distribution\\n" "${CROSS}" exit # exit the installer else printf " %b Continuing installation with unsupported RPM based distribution\\n" "${INFO}" fi fi # If neither apt-get or yum/dnf package managers were found else # it's not an OS we can support, printf " %b OS distribution not supported\\n" "${CROSS}" # so exit the installer exit fi } # A function for checking if a directory is a git repository is_repo() { # Use a named, local variable instead of the vague $1, which is the first argument passed to this function # These local variables should always be lowercase local directory="${1}" # A local variable for the current directory local curdir # A variable to store the return code local rc # Assign the current directory variable by using pwd curdir="${PWD}" # If the first argument passed to this function is a directory, if [[ -d "${directory}" ]]; then # move into the directory cd "${directory}" # Use git to check if the directory is a repo # git -C is not used here to support git versions older than 1.8.4 git status --short &> /dev/null || rc=$? # If the command was not successful, else # Set a non-zero return code if directory does not exist rc=1 fi # Move back into the directory the user started in cd "${curdir}" # Return the code; if one is not set, return 0 return "${rc:-0}" } # A function to clone a repo make_repo() { # Set named variables for better readability local directory="${1}" local remoteRepo="${2}" # The message to display when this function is running str="Clone ${remoteRepo} into ${directory}" # Display the message and use the color table to preface the message with an "info" indicator printf " %b %s..." "${INFO}" "${str}" # If the directory exists, if [[ -d "${directory}" ]]; then # delete everything in it so git can clone into it rm -rf "${directory}" fi # Clone the repo and return the return code from this command git clone -q --depth 20 "${remoteRepo}" "${directory}" &> /dev/null || return $? # Show a colored message showing it's status printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Always return 0? Not sure this is correct return 0 } # We need to make sure the repos are up-to-date so we can effectively install Clean out the directory if it exists for git to clone into update_repo() { # Use named, local variables # As you can see, these are the same variable names used in the last function, # but since they are local, their scope does not go beyond this function # This helps prevent the wrong value from being assigned if you were to set the variable as a GLOBAL one local directory="${1}" local curdir # A variable to store the message we want to display; # Again, it's useful to store these in variables in case we need to reuse or change the message; # we only need to make one change here local str="Update repo in ${1}" # Make sure we know what directory we are in so we can move back into it curdir="${PWD}" # Move into the directory that was passed as an argument cd "${directory}" &> /dev/null || return 1 # Let the user know what's happening printf " %b %s..." "${INFO}" "${str}" # Stash any local commits as they conflict with our working code git stash --all --quiet &> /dev/null || true # Okay for stash failure git clean --quiet --force -d || true # Okay for already clean directory # Pull the latest commits git pull --quiet &> /dev/null || return $? # Show a completion message printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Move back into the original directory cd "${curdir}" &> /dev/null || return 1 return 0 } # A function that combines the functions previously made getGitFiles() { # Setup named variables for the git repos # We need the directory local directory="${1}" # as well as the repo URL local remoteRepo="${2}" # A local variable containing the message to be displayed local str="Check for existing repository in ${1}" # Show the message printf " %b %s..." "${INFO}" "${str}" # Check if the directory is a repository if is_repo "${directory}"; then # Show that we're checking it printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Update the repo, returning an error message on failure update_repo "${directory}" || { printf "\\n %b: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } # If it's not a .git repo, else # Show an error printf "%b %b %s\\n" "${OVER}" "${CROSS}" "${str}" # Attempt to make the repository, showing an error on failure make_repo "${directory}" "${remoteRepo}" || { printf "\\n %bError: Could not update local repository. Contact support.%b\\n" "${COL_LIGHT_RED}" "${COL_NC}"; exit 1; } fi # echo a blank line echo "" # and return success? return 0 } # Reset a repo to get rid of any local changed resetRepo() { # Use named variables for arguments local directory="${1}" # Move into the directory cd "${directory}" &> /dev/null || return 1 # Store the message in a variable str="Resetting repository within ${1}..." # Show the message printf " %b %s..." "${INFO}" "${str}" # Use git to remove the local changes git reset --hard &> /dev/null || return $? # And show the status printf "%b %b %s\\n" "${OVER}" "${TICK}" "${str}" # Returning success anyway? return 0 } find_IPv4_information() { # Detects IPv4 address used for communication to WAN addresses. # Accepts no arguments, returns no values. # Named, local variables local route local IPv4bare # Find IP used to route to outside world by checking the the route to Google's public DNS server route=$(ip route get 8.8.8.8) # Get just the interface IPv4 address # shellcheck disable=SC2059,SC2086 # disabled as we intentionally want to split on whitespace and have printf populate # the variable with just the first field. printf -v IPv4bare "$(printf ${route#*src })" # Get the default gateway IPv4 address (the way to reach the Internet) # shellcheck disable=SC2059,SC2086 printf -v IPv4gw "$(printf ${route#*via })" if ! valid_ip "${IPv4bare}" ; then IPv4bare="127.0.0.1" fi # Append the CIDR notation to the IP address, if valid_ip fails this should return 127.0.0.1/8 IPV4_ADDRESS=$(ip -oneline -family inet address show | grep "${IPv4bare}/" | awk '{print $4}' | awk 'END {print}') } # Get available interfaces that are UP get_available_interfaces() { # There may be more than one so it's all stored in a variable availableInterfaces=$(ip --oneline link show up | grep -v "lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1) } # A function for displaying the dialogs the user sees when first running the installer welcomeDialogs() { # Display the welcome dialog using an appropriately sized window via the calculation conducted earlier in the script whiptail --msgbox --backtitle "Welcome" --title "Pi-hole automated installer" "\\n\\nThis installer will transform your device into a network-wide ad blocker!" ${r} ${c} # Request that users donate if they enjoy the software since we all work on it in our free time whiptail --msgbox --backtitle "Plea" --title "Free and open source" "\\n\\nThe Pi-hole is free, but powered by your donations: http://pi-hole.net/donate" ${r} ${c} # Explain the need for a static address whiptail --msgbox --backtitle "Initiating network interface" --title "Static IP Needed" "\\n\\nThe Pi-hole is a SERVER so it needs a STATIC IP ADDRESS to function properly. In the next section, you can choose to use your current network settings (DHCP) or to manually edit them." ${r} ${c} } # We need to make sure there is enough space before installing, so there is a function to check this verifyFreeDiskSpace() { # 50MB is the minimum space needed (45MB install (includes web admin bootstrap/jquery libraries etc) + 5MB one day of logs.) # - Fourdee: Local ensures the variable is only created, and accessible within this function/void. Generally considered a "good" coding practice for non-global variables. local str="Disk space check" # Required space in KB local required_free_kilobytes=51200 # Calculate existing free space on this machine local existing_free_kilobytes existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}') # If the existing space is not an integer, if ! [[ "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then # show an error that we can't determine the free space printf " %b %s\\n" "${CROSS}" "${str}" printf " %b Unknown free disk space! \\n" "${INFO}" printf " We were unable to determine available free disk space on this system.\\n" printf " You may override this check, however, it is not recommended.\\n" printf " The option '%b--i_do_not_follow_recommendations%b' can override this.\\n" "${COL_LIGHT_RED}" "${COL_NC}" printf " e.g: curl -L https://install.pi-hole.net | bash /dev/stdin %b