· 8 years ago · Feb 26, 2018, 03:16 PM
1#!/usr/bin/env bash
2# shellcheck disable=SC1090
3
4# Pi-hole: A black hole for Internet advertisements
5# (c) 2017-2018 Pi-hole, LLC (https://pi-hole.net)
6# Network-wide ad blocking via your own hardware.
7#
8# Installs and Updates Pi-hole
9#
10# This file is copyright under the latest version of the EUPL.
11# Please see LICENSE file for your rights under this license.
12
13# pi-hole.net/donate
14#
15# Install with this command (from your Linux machine):
16#
17# curl -sSL https://install.pi-hole.net | bash
18
19# -e option instructs bash to immediately exit if any command [1] has a non-zero exit status
20# We do not want users to end up with a partially working install, so we exit the script
21# instead of continuing the installation with something broken
22set -e
23
24######## VARIABLES #########
25# For better maintainability, we store as much information that can change in variables
26# This allows us to make a change in one place that can propogate to all instances of the variable
27# These variables should all be GLOBAL variables, written in CAPS
28# Local variables will be in lowercase and will exist only within functions
29# It's still a work in progress, so you may see some variance in this guideline until it is complete
30
31# Location for final installation log storage
32installLogLoc=/etc/pihole/install.log
33# This is an important file as it contains information specific to the machine it's being installed on
34setupVars=/etc/pihole/setupVars.conf
35# Pi-hole uses lighttpd as a Web server, and this is the config file for it
36# shellcheck disable=SC2034
37lighttpdConfig=/etc/lighttpd/lighttpd.conf
38# This is a file used for the colorized output
39coltable=/opt/pihole/COL_TABLE
40
41# We store several other folders and
42webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git"
43webInterfaceDir="/var/www/html/admin"
44piholeGitUrl="https://github.com/pi-hole/pi-hole.git"
45PI_HOLE_LOCAL_REPO="/etc/.pihole"
46# These are the names of piholes files, stored in an array
47PI_HOLE_FILES=(chronometer list piholeDebug piholeLogFlush setupLCD update version gravity uninstall webpage)
48# This folder is where the Pi-hole scripts will be installed
49PI_HOLE_INSTALL_DIR="/opt/pihole"
50useUpdateVars=false
51
52# Pi-hole needs an IP address; to begin, these variables are empty since we don't know what the IP is until
53# this script can run
54IPV4_ADDRESS=""
55IPV6_ADDRESS=""
56# By default, query logging is enabled and the dashboard is set to be installed
57QUERY_LOGGING=true
58INSTALL_WEB=true
59
60
61# Find the rows and columns will default to 80x24 if it can not be detected
62screen_size=$(stty size 2>/dev/null || echo 24 80)
63rows=$(echo "${screen_size}" | awk '{print $1}')
64columns=$(echo "${screen_size}" | awk '{print $2}')
65
66# Divide by two so the dialogs take up half of the screen, which looks nice.
67r=$(( rows / 2 ))
68c=$(( columns / 2 ))
69# Unless the screen is tiny
70r=$(( r < 20 ? 20 : r ))
71c=$(( c < 70 ? 70 : c ))
72
73######## Undocumented Flags. Shhh ########
74# These are undocumented flags; some of which we can use when repairing an installation
75# The runUnattended flag is one example of this
76skipSpaceCheck=false
77reconfigure=false
78runUnattended=false
79
80# If the color table file exists,
81if [[ -f "${coltable}" ]]; then
82 # source it
83 source ${coltable}
84# Othwerise,
85else
86 # Set these values so the installer can still run in color
87 COL_NC='\e[0m' # No Color
88 COL_LIGHT_GREEN='\e[1;32m'
89 COL_LIGHT_RED='\e[1;31m'
90 TICK="[${COL_LIGHT_GREEN}✓${COL_NC}]"
91 CROSS="[${COL_LIGHT_RED}✗${COL_NC}]"
92 INFO="[i]"
93 # shellcheck disable=SC2034
94 DONE="${COL_LIGHT_GREEN} done!${COL_NC}"
95 OVER="\\r\\033[K"
96fi
97
98# A simple function that just echoes out our logo in ASCII format
99# This lets users know that it is a Pi-hole, LLC product
100show_ascii_berry() {
101 echo -e "
102 ${COL_LIGHT_GREEN}.;;,.
103 .ccccc:,.
104 :cccclll:. ..,,
105 :ccccclll. ;ooodc
106 'ccll:;ll .oooodc
107 .;cll.;;looo:.
108 ${COL_LIGHT_RED}.. ','.
109 .',,,,,,'.
110 .',,,,,,,,,,.
111 .',,,,,,,,,,,,....
112 ....''',,,,,,,'.......
113 ......... .... .........
114 .......... ..........
115 .......... ..........
116 ......... .... .........
117 ........,,,,,,,'......
118 ....',,,,,,,,,,,,.
119 .',,,,,,,,,'.
120 .',,,,,,'.
121 ..'''.${COL_NC}
122"
123}
124
125# Compatibility
126distro_check() {
127# If apt-get is installed, then we know it's part of the Debian family
128if command -v apt-get &> /dev/null; then
129 # Set some global variables here
130 # We don't set them earlier since the family might be Red Hat, so these values would be different
131 PKG_MANAGER="apt-get"
132 # A variable to store the command used to update the package cache
133 UPDATE_PKG_CACHE="${PKG_MANAGER} update"
134 # An array for something...
135 PKG_INSTALL=(${PKG_MANAGER} --yes --no-install-recommends install)
136 # grep -c will return 1 retVal on 0 matches, block this throwing the set -e with an OR TRUE
137 PKG_COUNT="${PKG_MANAGER} -s -o Debug::NoLocking=true upgrade | grep -c ^Inst || true"
138 # Some distros vary slightly so these fixes for dependencies may apply
139 # Debian 7 doesn't have iproute2 so if the dry run install is successful,
140 if ${PKG_MANAGER} install --dry-run iproute2 > /dev/null 2>&1; then
141 # we can install it
142 iproute_pkg="iproute2"
143 # Otherwise,
144 else
145 # use iproute
146 iproute_pkg="iproute"
147 fi
148 # We prefer the php metapackage if it's there
149 if ${PKG_MANAGER} install --dry-run php > /dev/null 2>&1; then
150 phpVer="php"
151 # If not,
152 else
153 # fall back on the php5 packages
154 phpVer="php5"
155 fi
156 # We also need the correct version for `php-sqlite` (which differs across distros)
157 if ${PKG_MANAGER} install --dry-run ${phpVer}-sqlite3 > /dev/null 2>&1; then
158 phpSqlite="sqlite3"
159 else
160 phpSqlite="sqlite"
161 fi
162 # Since our install script is so large, we need several other programs to successfuly get a machine provisioned
163 # These programs are stored in an array so they can be looped through later
164 INSTALLER_DEPS=(apt-utils dialog debconf dhcpcd5 git ${iproute_pkg} whiptail)
165 # Pi-hole itself has several dependencies that also need to be installed
166 PIHOLE_DEPS=(bc cron curl dnsmasq dnsutils iputils-ping lsof netcat sudo unzip wget idn2)
167 # The Web dashboard has some that also need to be installed
168 # It's useful to separate the two since our repos are also setup as "Core" code and "Web" code
169 PIHOLE_WEB_DEPS=(lighttpd ${phpVer}-common ${phpVer}-cgi ${phpVer}-${phpSqlite})
170 # The Web server user,
171 LIGHTTPD_USER="www-data"
172 # group,
173 LIGHTTPD_GROUP="www-data"
174 # and config file
175 LIGHTTPD_CFG="lighttpd.conf.debian"
176 # The DNS server user
177 DNSMASQ_USER="dnsmasq"
178
179 # A function to check...
180 test_dpkg_lock() {
181 # An iterator used for counting loop iterations
182 i=0
183 # fuser is a program to show which processes use the named files, sockets, or filesystems
184 # So while the command is true
185 while fuser /var/lib/dpkg/lock >/dev/null 2>&1 ; do
186 # Wait half a second
187 sleep 0.5
188 # and increase the iterator
189 ((i=i+1))
190 done
191 # Always return success, since we only return if there is no
192 # lock (anymore)
193 return 0
194 }
195
196# If apt-get is not found, check for rpm to see if it's a Red Hat family OS
197elif command -v rpm &> /dev/null; then
198 # Then check if dnf or yum is the package manager
199 if command -v dnf &> /dev/null; then
200 PKG_MANAGER="dnf"
201 else
202 PKG_MANAGER="yum"
203 fi
204
205 # Fedora and family update cache on every PKG_INSTALL call, no need for a separate update.
206 UPDATE_PKG_CACHE=":"
207 PKG_INSTALL=(${PKG_MANAGER} install -y)
208 PKG_COUNT="${PKG_MANAGER} check-update | egrep '(.i686|.x86|.noarch|.arm|.src)' | wc -l"
209 INSTALLER_DEPS=(dialog git iproute net-tools newt procps-ng)
210 PIHOLE_DEPS=(bc bind-utils cronie curl dnsmasq findutils nmap-ncat sudo unzip wget libidn2 psmisc)
211 PIHOLE_WEB_DEPS=(lighttpd lighttpd-fastcgi php php-common php-cli php-pdo)
212 # EPEL (https://fedoraproject.org/wiki/EPEL) is required for lighttpd on CentOS
213 if grep -qi 'centos' /etc/redhat-release; then
214 INSTALLER_DEPS=("${INSTALLER_DEPS[@]}" "epel-release");
215 fi
216 LIGHTTPD_USER="lighttpd"
217 LIGHTTPD_GROUP="lighttpd"
218 LIGHTTPD_CFG="lighttpd.conf.fedora"
219 DNSMASQ_USER="nobody"
220
221# If neither apt-get or rmp/dnf are found
222else
223 # it's not an OS we can support,
224 echo -e " ${CROSS} OS distribution not supported"
225 # so exit the installer
226 exit
227fi
228}
229
230# A function for checking if a folder is a git repository
231is_repo() {
232 # Use a named, local variable instead of the vague $1, which is the first arguement passed to this function
233 # These local variables should always be lowercase
234 local directory="${1}"
235 # A local variable for the current directory
236 local curdir
237 # A variable to store the return code
238 local rc
239 # Assign the current directory variable by using pwd
240 curdir="${PWD}"
241 # If the first argument passed to this function is a directory,
242 if [[ -d "${directory}" ]]; then
243 # move into the directory
244 cd "${directory}"
245 # Use git to check if the folder is a repo
246 # git -C is not used here to support git versions older than 1.8.4
247 git status --short &> /dev/null || rc=$?
248 # If the command was not successful,
249 else
250 # Set a non-zero return code if directory does not exist
251 rc=1
252 fi
253 # Move back into the directory the user started in
254 cd "${curdir}"
255 # Return the code; if one is not set, return 0
256 return "${rc:-0}"
257}
258
259# A function to clone a repo
260make_repo() {
261 # Set named variables for better readability
262 local directory="${1}"
263 local remoteRepo="${2}"
264 # The message to display when this function is running
265 str="Clone ${remoteRepo} into ${directory}"
266 # Display the message and use the color table to preface the message with an "info" indicator
267 echo -ne " ${INFO} ${str}..."
268 # If the directory exists,
269 if [[ -d "${directory}" ]]; then
270 # delete everything in it so git can clone into it
271 rm -rf "${directory}"
272 fi
273 # Clone the repo and return the return code from this command
274 git clone -q --depth 1 "${remoteRepo}" "${directory}" &> /dev/null || return $?
275 # Show a colored message showing it's status
276 echo -e "${OVER} ${TICK} ${str}"
277 # Always return 0? Not sure this is correct
278 return 0
279}
280
281# 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
282update_repo() {
283 # Use named, local variables
284 # As you can see, these are the same variable names used in the last function,
285 # but since they are local, their scope does not go beyond this function
286 # This helps prevent the wrong value from being assigned if you were to set the variable as a GLOBAL one
287 local directory="${1}"
288 local curdir
289
290 # A variable to store the message we want to display;
291 # Again, it's useful to store these in variables in case we need to reuse or change the message;
292 # we only need to make one change here
293 local str="Update repo in ${1}"
294
295 # Make sure we know what directory we are in so we can move back into it
296 curdir="${PWD}"
297 # Move into the directory that was passed as an argument
298 cd "${directory}" &> /dev/null || return 1
299 # Let the user know what's happening
300 echo -ne " ${INFO} ${str}..."
301 # Stash any local commits as they conflict with our working code
302 git stash --all --quiet &> /dev/null || true # Okay for stash failure
303 git clean --quiet --force -d || true # Okay for already clean directory
304 # Pull the latest commits
305 git pull --quiet &> /dev/null || return $?
306 # Show a completion message
307 echo -e "${OVER} ${TICK} ${str}"
308 # Move back into the oiginal directory
309 cd "${curdir}" &> /dev/null || return 1
310 return 0
311}
312
313# A function that combines the functions previously made
314getGitFiles() {
315 # Setup named variables for the git repos
316 # We need the directory
317 local directory="${1}"
318 # as well as the repo URL
319 local remoteRepo="${2}"
320 # A local varible containing the message to be displayed
321 local str="Check for existing repository in ${1}"
322 # Show the message
323 echo -ne " ${INFO} ${str}..."
324 # Check if the directory is a repository
325 if is_repo "${directory}"; then
326 # Show that we're checking it
327 echo -e "${OVER} ${TICK} ${str}"
328 # Update the repo, returning an error message on failure
329 update_repo "${directory}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; }
330 # If it's not a .git repo,
331 else
332 # Show an error
333 echo -e "${OVER} ${CROSS} ${str}"
334 # Attempt to make the repository, showing an error on falure
335 make_repo "${directory}" "${remoteRepo}" || { echo -e "\\n ${COL_LIGHT_RED}Error: Could not update local repository. Contact support.${COL_NC}"; exit 1; }
336 fi
337 # echo a blank line
338 echo ""
339 # and return success?
340 return 0
341}
342
343# Reset a repo to get rid of any local changed
344resetRepo() {
345 # Use named varibles for arguments
346 local directory="${1}"
347 # Move into the directory
348 cd "${directory}" &> /dev/null || return 1
349 # Store the message in a varible
350 str="Resetting repository within ${1}..."
351 # Show the message
352 echo -ne " ${INFO} ${str}"
353 # Use git to remove the local changes
354 git reset --hard &> /dev/null || return $?
355 # And show the status
356 echo -e "${OVER} ${TICK} ${str}"
357 # Returning success anyway?
358 return 0
359}
360
361# We need to know the IPv4 information so we can effectively setup the DNS server
362# Without this information, we won't know where to Pi-hole will be found
363find_IPv4_information() {
364 # Named, local variables
365 local route
366 # Find IP used to route to outside world by checking the the route to Google's public DNS server
367 route=$(ip route get 8.8.8.8)
368 # Use awk to strip out just the interface device as it is used in future commands
369 IPv4dev=$(awk '{for (i=1; i<=NF; i++) if ($i~/dev/) print $(i+1)}' <<< "${route}")
370 # Get just the IP address
371 IPv4bare=$(awk '{print $7}' <<< "${route}")
372 # Append the CIDR notation to the IP address
373 IPV4_ADDRESS=$(ip -o -f inet addr show | grep "${IPv4bare}" | awk '{print $4}' | awk 'END {print}')
374 # Get the default gateway (the way to reach the Internet)
375 IPv4gw=$(awk '{print $3}' <<< "${route}")
376
377}
378
379# Get available interfaces that are UP
380get_available_interfaces() {
381 # There may be more than one so it's all stored in a variable
382 availableInterfaces=$(ip --oneline link show up | grep -v "lo" | awk '{print $2}' | cut -d':' -f1 | cut -d'@' -f1)
383}
384
385# A function for displaying the dialogs the user sees when first running the installer
386welcomeDialogs() {
387 # Display the welcome dialog using an approriately sized window via the calculation conducted earlier in the script
388 whiptail --msgbox --backtitle "Welcome" --title "Pi-hole automated installer" "\\n\\nThis installer will transform your device into a network-wide ad blocker!" ${r} ${c}
389
390 # Request that users donate if they enjoy the software since we all work on it in our free time
391 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}
392
393 # Explain the need for a static address
394 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.
395
396In the next section, you can choose to use your current network settings (DHCP) or to manually edit them." ${r} ${c}
397}
398
399# We need to make sure there is enough space before installing, so there is a function to check this
400verifyFreeDiskSpace() {
401
402 # 50MB is the minimum space needed (45MB install (includes web admin bootstrap/jquery libraries etc) + 5MB one day of logs.)
403 # - Fourdee: Local ensures the variable is only created, and accessible within this function/void. Generally considered a "good" coding practice for non-global variables.
404 local str="Disk space check"
405 # Reqired space in KB
406 local required_free_kilobytes=51200
407 # Calculate existing free space on this machine
408 local existing_free_kilobytes
409 existing_free_kilobytes=$(df -Pk | grep -m1 '\/$' | awk '{print $4}')
410
411 # If the existing space is not an integer,
412 if ! [[ "${existing_free_kilobytes}" =~ ^([0-9])+$ ]]; then
413 # show an error that we can't determine the free space
414 echo -e " ${CROSS} ${str}
415 Unknown free disk space!
416 We were unable to determine available free disk space on this system.
417 You may override this check, however, it is not recommended
418 The option '${COL_LIGHT_RED}--i_do_not_follow_recommendations${COL_NC}' can override this
419 e.g: curl -L https://install.pi-hole.net | bash /dev/stdin ${COL_LIGHT_RED}<option>${COL_NC}"
420 # exit with an error code
421 exit 1
422 # If there is insufficient free disk space,
423 elif [[ "${existing_free_kilobytes}" -lt "${required_free_kilobytes}" ]]; then
424 # show an error message
425 echo -e " ${CROSS} ${str}
426 Your system disk appears to only have ${existing_free_kilobytes} KB free
427 It is recommended to have a minimum of ${required_free_kilobytes} KB to run the Pi-hole"
428 # if the vcgencmd command exists,
429 if command -v vcgencmd &> /dev/null; then
430 # it's probably a Raspbian install, so show a message about expanding the filesystem
431 echo " If this is a new install you may need to expand your disk
432 Run 'sudo raspi-config', and choose the 'expand file system' option
433 After rebooting, run this installation again
434 e.g: curl -L https://install.pi-hole.net | bash"
435 fi
436 # Show there is not enough free space
437 echo -e "\\n ${COL_LIGHT_RED}Insufficient free space, exiting...${COL_NC}"
438 # and exit with an error
439 exit 1
440 # Otherwise,
441 else
442 # Show that we're running a disk space check
443 echo -e " ${TICK} ${str}"
444 fi
445}
446
447# A function that let's the user pick an interface to use with Pi-hole
448chooseInterface() {
449 # Turn the available interfaces into an array so it can be used with a whiptail dialog
450 local interfacesArray=()
451 # Number of available interfaces
452 local interfaceCount
453 # Whiptail variable storage
454 local chooseInterfaceCmd
455 # Temporary Whiptail options storage
456 local chooseInterfaceOptions
457 # Loop sentinel variable
458 local firstLoop=1
459
460 # Find out how many interfaces are available to choose from
461 interfaceCount=$(echo "${availableInterfaces}" | wc -l)
462
463 # If there is one interface,
464 if [[ "${interfaceCount}" -eq 1 ]]; then
465 # Set it as the interface to use since there is no other option
466 PIHOLE_INTERFACE="${availableInterfaces}"
467 # Otherwise,
468 else
469 # While reading through the available interfaces
470 while read -r line; do
471 # use a variable to set the option as OFF to begin with
472 mode="OFF"
473 # If it's the first loop,
474 if [[ "${firstLoop}" -eq 1 ]]; then
475 # set this as the interface to use (ON)
476 firstLoop=0
477 mode="ON"
478 fi
479 # Put all these interfaces into an array
480 interfacesArray+=("${line}" "available" "${mode}")
481 # Feed the available interfaces into this while loop
482 done <<< "${availableInterfaces}"
483 # The whiptail command that will be run, stored in a variable
484 chooseInterfaceCmd=(whiptail --separate-output --radiolist "Choose An Interface (press space to select)" ${r} ${c} ${interfaceCount})
485 # Now run the command using the interfaces saved into the array
486 chooseInterfaceOptions=$("${chooseInterfaceCmd[@]}" "${interfacesArray[@]}" 2>&1 >/dev/tty) || \
487 # If the user chooses Canel, exit
488 { echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
489 # For each interface
490 for desiredInterface in ${chooseInterfaceOptions}; do
491 # Set the one the user selected as the interface to use
492 PIHOLE_INTERFACE=${desiredInterface}
493 # and show this information to the user
494 echo -e " ${INFO} Using interface: $PIHOLE_INTERFACE"
495 done
496 fi
497}
498
499# This lets us prefer ULA addresses over GUA
500# This caused problems for some users when their ISP changed their IPv6 addresses
501# See https://github.com/pi-hole/pi-hole/issues/1473#issuecomment-301745953
502testIPv6() {
503 # first will contain fda2 (ULA)
504 first="$(cut -f1 -d":" <<< "$1")"
505 # value1 will contain 253 which is the decimal value corresponding to 0xfd
506 value1=$(( (0x$first)/256 ))
507 # will contain 162 which is the decimal value corresponding to 0xa2
508 value2=$(( (0x$first)%256 ))
509 # the ULA test is testing for fc00::/7 according to RFC 4193
510 if (( (value1&254)==252 )); then
511 echo "ULA"
512 fi
513 # the GUA test is testing for 2000::/3 according to RFC 4291
514 if (( (value1&112)==32 )); then
515 echo "GUA"
516 fi
517 # the LL test is testing for fe80::/10 according to RFC 4193
518 if (( (value1)==254 )) && (( (value2&192)==128 )); then
519 echo "Link-local"
520 fi
521}
522
523# A dialog for showing the user about IPv6 blocking
524useIPv6dialog() {
525 # Determine the IPv6 address used for blocking
526 IPV6_ADDRESSES=($(ip -6 address | grep 'scope global' | awk '{print $2}'))
527
528 # For each address in the array above, determine the type of IPv6 address it is
529 for i in "${IPV6_ADDRESSES[@]}"; do
530 # Check if it's ULA, GUA, or LL by using the function created earlier
531 result=$(testIPv6 "$i")
532 # If it's a ULA address, use it and store it as a global variable
533 [[ "${result}" == "ULA" ]] && ULA_ADDRESS="${i%/*}"
534 # If it's a GUA address, we can still use it si store it as a global variable
535 [[ "${result}" == "GUA" ]] && GUA_ADDRESS="${i%/*}"
536 done
537
538 # Determine which address to be used: Prefer ULA over GUA or don't use any if none found
539 # If the ULA_ADDRESS contains a value,
540 if [[ ! -z "${ULA_ADDRESS}" ]]; then
541 # set the IPv6 address to the ULA address
542 IPV6_ADDRESS="${ULA_ADDRESS}"
543 # Show this info to the user
544 echo -e " ${INFO} Found IPv6 ULA address, using it for blocking IPv6 ads"
545 # Otherwise, if the GUA_ADDRESS has a value,
546 elif [[ ! -z "${GUA_ADDRESS}" ]]; then
547 # Let the user know
548 echo -e " ${INFO} Found IPv6 GUA address, using it for blocking IPv6 ads"
549 # And assign it to the global variable
550 IPV6_ADDRESS="${GUA_ADDRESS}"
551 # If none of those work,
552 else
553 # explain that IPv6 blocking will not be used
554 echo -e " ${INFO} Unable to find IPv6 ULA/GUA address, IPv6 adblocking will not be enabled"
555 # So set the variable to be empty
556 IPV6_ADDRESS=""
557 fi
558
559 # If the IPV6_ADDRESS contains a value
560 if [[ ! -z "${IPV6_ADDRESS}" ]]; then
561 # Display that IPv6 is supported and will be used
562 whiptail --msgbox --backtitle "IPv6..." --title "IPv6 Supported" "$IPV6_ADDRESS will be used to block ads." ${r} ${c}
563 fi
564}
565
566# A function to check if we should use IPv4 and/or IPv6 for blocking ads
567use4andor6() {
568 # Named local variables
569 local useIPv4
570 local useIPv6
571 # Let use select IPv4 and/or IPv6 via a checklist
572 cmd=(whiptail --separate-output --checklist "Select Protocols (press space to select)" ${r} ${c} 2)
573 # In an array, show the options available:
574 # IPv4 (on by default)
575 options=(IPv4 "Block ads over IPv4" on
576 # or IPv6 (on by default if available)
577 IPv6 "Block ads over IPv6" on)
578 # In a variable, show the choices available; exit if Cancel is selected
579 choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) || { echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
580 # For each choice available,
581 for choice in ${choices}
582 do
583 # Set the values to true
584 case ${choice} in
585 IPv4 ) useIPv4=true;;
586 IPv6 ) useIPv6=true;;
587 esac
588 done
589 # If IPv4 is to be used,
590 if [[ "${useIPv4}" ]]; then
591 # Run our function to get the information we need
592 find_IPv4_information
593 getStaticIPv4Settings
594 setStaticIPv4
595 fi
596 # If IPv6 is to be used,
597 if [[ "${useIPv6}" ]]; then
598 # Run our function to get this information
599 useIPv6dialog
600 fi
601 # Echo the information to the user
602 echo -e " ${INFO} IPv4 address: ${IPV4_ADDRESS}"
603 echo -e " ${INFO} IPv6 address: ${IPV6_ADDRESS}"
604 # If neither protocol is selected,
605 if [[ ! "${useIPv4}" ]] && [[ ! "${useIPv6}" ]]; then
606 # Show an error in red
607 echo -e " ${COL_LIGHT_RED}Error: Neither IPv4 or IPv6 selected${COL_NC}"
608 # and exit with an error
609 exit 1
610 fi
611}
612
613#
614getStaticIPv4Settings() {
615 # Local, named variables
616 local ipSettingsCorrect
617 # Ask if the user wants to use DHCP settings as their static IP
618 # This is useful for users that are using DHCP reservations; then we can just use the information gathered via our functions
619 if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Do you want to use your current network settings as a static address?
620 IP address: ${IPV4_ADDRESS}
621 Gateway: ${IPv4gw}" ${r} ${c}; then
622 # If they choose yes, let the user know that the IP address will not be available via DHCP and may cause a conflict.
623 whiptail --msgbox --backtitle "IP information" --title "FYI: IP Conflict" "It is possible your router could still try to assign this IP to a device, which would cause a conflict. But in most cases the router is smart enough to not do that.
624If you are worried, either manually set the address, or modify the DHCP reservation pool so it does not include the IP you want.
625It is also possible to use a DHCP reservation, but if you are going to do that, you might as well set a static address." ${r} ${c}
626 # Nothing else to do since the variables are already set above
627 else
628 # Otherwise, we need to ask the user to input their desired settings.
629 # Start by getting the IPv4 address (pre-filling it with info gathered from DHCP)
630 # Start a loop to let the user enter their information with the chance to go back and edit it if necessary
631 until [[ "${ipSettingsCorrect}" = True ]]; do
632
633 # Ask for the IPv4 address
634 IPV4_ADDRESS=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 address" --inputbox "Enter your desired IPv4 address" ${r} ${c} "${IPV4_ADDRESS}" 3>&1 1>&2 2>&3) || \
635 # Cancelling IPv4 settings window
636 { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
637 echo -e " ${INFO} Your static IPv4 address: ${IPV4_ADDRESS}"
638
639 # Ask for the gateway
640 IPv4gw=$(whiptail --backtitle "Calibrating network interface" --title "IPv4 gateway (router)" --inputbox "Enter your desired IPv4 default gateway" ${r} ${c} "${IPv4gw}" 3>&1 1>&2 2>&3) || \
641 # Cancelling gateway settings window
642 { ipSettingsCorrect=False; echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
643 echo -e " ${INFO} Your static IPv4 gateway: ${IPv4gw}"
644
645 # Give the user a chance to review their settings before moving on
646 if whiptail --backtitle "Calibrating network interface" --title "Static IP Address" --yesno "Are these settings correct?
647 IP address: ${IPV4_ADDRESS}
648 Gateway: ${IPv4gw}" ${r} ${c}; then
649 # After that's done, the loop ends and we move on
650 ipSettingsCorrect=True
651 else
652 # If the settings are wrong, the loop continues
653 ipSettingsCorrect=False
654 fi
655 done
656 # End the if statement for DHCP vs. static
657 fi
658}
659
660# dhcpcd is very annoying,
661setDHCPCD() {
662 # but we can append these lines to dhcpcd.conf to enable a static IP
663 echo "interface ${PIHOLE_INTERFACE}
664 static ip_address=${IPV4_ADDRESS}
665 static routers=${IPv4gw}
666 static domain_name_servers=127.0.0.1" | tee -a /etc/dhcpcd.conf >/dev/null
667}
668
669setStaticIPv4() {
670 # Local, named variables
671 local IFCFG_FILE
672 local IPADDR
673 local CIDR
674 # For the Debian family, if dhcpcd.conf exists,
675 if [[ -f "/etc/dhcpcd.conf" ]]; then
676 # check if the IP is already in the file
677 if grep -q "${IPV4_ADDRESS}" /etc/dhcpcd.conf; then
678 echo -e " ${INFO} Static IP already configured"
679 # If it's not,
680 else
681 # set it using our function
682 setDHCPCD
683 # Then use the ip command to immediately set the new address
684 ip addr replace dev "${PIHOLE_INTERFACE}" "${IPV4_ADDRESS}"
685 # Also give a warning that the user may need to reboot their system
686 echo -e " ${TICK} Set IP address to ${IPV4_ADDRESS%/*}
687 You may need to restart after the install is complete"
688 fi
689 # If it's not Debian, check if it's the Fedora family by checking for the file below
690 elif [[ -f "/etc/sysconfig/network-scripts/ifcfg-${PIHOLE_INTERFACE}" ]];then
691 # If it exists,
692 IFCFG_FILE=/etc/sysconfig/network-scripts/ifcfg-${PIHOLE_INTERFACE}
693 # check if the desired IP is already set
694 if grep -q "${IPV4_ADDRESS}" "${IFCFG_FILE}"; then
695 echo -e " ${INFO} Static IP already configured"
696 # Otherwise,
697 else
698 # Put the IP in variables without the CIDR notation
699 IPADDR=$(echo "${IPV4_ADDRESS}" | cut -f1 -d/)
700 CIDR=$(echo "${IPV4_ADDRESS}" | cut -f2 -d/)
701 # Backup existing interface configuration:
702 cp "${IFCFG_FILE}" "${IFCFG_FILE}".pihole.orig
703 # Build Interface configuration file using the GLOBAL variables we have
704 {
705 echo "# Configured via Pi-hole installer"
706 echo "DEVICE=$PIHOLE_INTERFACE"
707 echo "BOOTPROTO=none"
708 echo "ONBOOT=yes"
709 echo "IPADDR=$IPADDR"
710 echo "PREFIX=$CIDR"
711 echo "GATEWAY=$IPv4gw"
712 echo "DNS1=$PIHOLE_DNS_1"
713 echo "DNS2=$PIHOLE_DNS_2"
714 echo "USERCTL=no"
715 }> "${IFCFG_FILE}"
716 # Use ip to immediately set the new address
717 ip addr replace dev "${PIHOLE_INTERFACE}" "${IPV4_ADDRESS}"
718 # If NetworkMangler command line interface exists and ready to mangle,
719 if command -v nmcli &> /dev/null && nmcli general status &> /dev/null; then
720 # Tell NetworkManagler to read our new sysconfig file
721 nmcli con load "${IFCFG_FILE}" > /dev/null
722 fi
723 # Show a warning that the user may need to restart
724 echo -e " ${TICK} Set IP address to ${IPV4_ADDRESS%/*}
725 You may need to restart after the install is complete"
726 fi
727 # If all that fails,
728 else
729 # show an error and exit
730 echo -e " ${INFO} Warning: Unable to locate configuration file to set static IPv4 address"
731 exit 1
732 fi
733}
734
735# Check an IP address to see if it is a valid one
736valid_ip() {
737 # Local, named variables
738 local ip=${1}
739 local stat=1
740
741 # If the IP matches the format xxx.xxx.xxx.xxx,
742 if [[ "${ip}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
743 # Save the old Interfal Field Separator in a variable
744 OIFS=$IFS
745 # and set the new one to a dot (period)
746 IFS='.'
747 # Put the IP into an array
748 ip=(${ip})
749 # Restore the IFS to what it was
750 IFS=${OIFS}
751 ## Evaluate each octet by checking if it's less than or equal to 255 (the max for each octet)
752 [[ "${ip[0]}" -le 255 && "${ip[1]}" -le 255 \
753 && "${ip[2]}" -le 255 && "${ip[3]}" -le 255 ]]
754 # Save the exit code
755 stat=$?
756 fi
757 # Return the exit code
758 return ${stat}
759}
760
761# A function to choose the upstream DNS provider(s)
762setDNS() {
763 # Local, named variables
764 local DNSSettingsCorrect
765
766 # In an array, list the available upstream providers
767 DNSChooseOptions=(Google ""
768 OpenDNS ""
769 Level3 ""
770 Norton ""
771 Comodo ""
772 DNSWatch ""
773 Quad9 ""
774 Custom "")
775 # In a whiptail dialog, show the options
776 DNSchoices=$(whiptail --separate-output --menu "Select Upstream DNS Provider. To use your own, select Custom." ${r} ${c} 7 \
777 "${DNSChooseOptions[@]}" 2>&1 >/dev/tty) || \
778 # exit if Cancel is selected
779 { echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
780
781 # Display the selection
782 echo -ne " ${INFO} Using "
783 # Depending on the user's choice, set the GLOBAl variables to the IP of the respective provider
784 case ${DNSchoices} in
785 Google)
786 echo "Google DNS servers"
787 PIHOLE_DNS_1="8.8.8.8"
788 PIHOLE_DNS_2="8.8.4.4"
789 ;;
790 OpenDNS)
791 echo "OpenDNS servers"
792 PIHOLE_DNS_1="208.67.222.222"
793 PIHOLE_DNS_2="208.67.220.220"
794 ;;
795 Level3)
796 echo "Level3 servers"
797 PIHOLE_DNS_1="4.2.2.1"
798 PIHOLE_DNS_2="4.2.2.2"
799 ;;
800 Norton)
801 echo "Norton ConnectSafe servers"
802 PIHOLE_DNS_1="199.85.126.10"
803 PIHOLE_DNS_2="199.85.127.10"
804 ;;
805 Comodo)
806 echo "Comodo Secure servers"
807 PIHOLE_DNS_1="8.26.56.26"
808 PIHOLE_DNS_2="8.20.247.20"
809 ;;
810 DNSWatch)
811 echo "DNS.WATCH servers"
812 PIHOLE_DNS_1="84.200.69.80"
813 PIHOLE_DNS_2="84.200.70.40"
814 ;;
815 Quad9)
816 echo "Quad9 servers"
817 PIHOLE_DNS_1="9.9.9.9"
818 PIHOLE_DNS_2="149.112.112.112"
819 ;;
820 Custom)
821 # Until the DNS settings are selected,
822 until [[ "${DNSSettingsCorrect}" = True ]]; do
823 #
824 strInvalid="Invalid"
825 # If the first
826 if [[ ! "${PIHOLE_DNS_1}" ]]; then
827 # and second upstream servers do not exist
828 if [[ ! "${PIHOLE_DNS_2}" ]]; then
829 #
830 prePopulate=""
831 # Otherwise,
832 else
833 #
834 prePopulate=", ${PIHOLE_DNS_2}"
835 fi
836 #
837 elif [[ "${PIHOLE_DNS_1}" ]] && [[ ! "${PIHOLE_DNS_2}" ]]; then
838 #
839 prePopulate="${PIHOLE_DNS_1}"
840 #
841 elif [[ "${PIHOLE_DNS_1}" ]] && [[ "${PIHOLE_DNS_2}" ]]; then
842 #
843 prePopulate="${PIHOLE_DNS_1}, ${PIHOLE_DNS_2}"
844 fi
845
846 # Dialog for the user to enter custom upstream servers
847 piholeDNS=$(whiptail --backtitle "Specify Upstream DNS Provider(s)" --inputbox "Enter your desired upstream DNS provider(s), seperated by a comma.\\n\\nFor example '8.8.8.8, 8.8.4.4'" ${r} ${c} "${prePopulate}" 3>&1 1>&2 2>&3) || \
848 { echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
849 #
850 PIHOLE_DNS_1=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$1}')
851 PIHOLE_DNS_2=$(echo "${piholeDNS}" | sed 's/[, \t]\+/,/g' | awk -F, '{print$2}')
852 # If the IP is valid,
853 if ! valid_ip "${PIHOLE_DNS_1}" || [[ ! "${PIHOLE_DNS_1}" ]]; then
854 # store it in the variable so we can use it
855 PIHOLE_DNS_1=${strInvalid}
856 fi
857 # Do the same for the secondary server
858 if ! valid_ip "${PIHOLE_DNS_2}" && [[ "${PIHOLE_DNS_2}" ]]; then
859 PIHOLE_DNS_2=${strInvalid}
860 fi
861 # If either of the DNS servers are invalid,
862 if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]] || [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then
863 # explain this to the user
864 whiptail --msgbox --backtitle "Invalid IP" --title "Invalid IP" "One or both entered IP addresses were invalid. Please try again.\\n\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c}
865 # and set the variables back to nothing
866 if [[ "${PIHOLE_DNS_1}" == "${strInvalid}" ]]; then
867 PIHOLE_DNS_1=""
868 fi
869 if [[ "${PIHOLE_DNS_2}" == "${strInvalid}" ]]; then
870 PIHOLE_DNS_2=""
871 fi
872 # Since the settings will not work, stay in the loop
873 DNSSettingsCorrect=False
874 # Othwerise,
875 else
876 # Show the settings
877 if (whiptail --backtitle "Specify Upstream DNS Provider(s)" --title "Upstream DNS Provider(s)" --yesno "Are these settings correct?\\n DNS Server 1: $PIHOLE_DNS_1\\n DNS Server 2: ${PIHOLE_DNS_2}" ${r} ${c}); then
878 # and break from the loop since the servers are vaid
879 DNSSettingsCorrect=True
880 # Otherwise,
881 else
882 # If the settings are wrong, the loop continues
883 DNSSettingsCorrect=False
884 fi
885 fi
886 done
887 ;;
888 esac
889}
890
891# Allow the user to enable/disable logging
892setLogging() {
893 # Local, named variables
894 local LogToggleCommand
895 local LogChooseOptions
896 local LogChoices
897
898 # Ask if the user wants to log queries
899 LogToggleCommand=(whiptail --separate-output --radiolist "Do you want to log queries?\\n (Disabling will render graphs on the Admin page useless):" ${r} ${c} 6)
900 # The default selection is on
901 LogChooseOptions=("On (Recommended)" "" on
902 Off "" off)
903 # Get the user's choice
904 LogChoices=$("${LogToggleCommand[@]}" "${LogChooseOptions[@]}" 2>&1 >/dev/tty) || (echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}" && exit 1)
905 case ${LogChoices} in
906 # If it's on
907 "On (Recommended)")
908 echo -e " ${INFO} Logging On."
909 # Set the GLOBAL variable to true so we know what they selected
910 QUERY_LOGGING=true
911 ;;
912 # Othwerise, it's off,
913 Off)
914 echo -e " ${INFO} Logging Off."
915 # So set it to false
916 QUERY_LOGGING=false
917 ;;
918 esac
919}
920
921# Funtion to ask the user if they want to install the dashboard
922setAdminFlag() {
923 # Local, named variables
924 local WebToggleCommand
925 local WebChooseOptions
926 local WebChoices
927
928 # Similar to the logging function, ask what the user wants
929 WebToggleCommand=(whiptail --separate-output --radiolist "Do you wish to install the web admin interface?" ${r} ${c} 6)
930 # with the default being enabled
931 WebChooseOptions=("On (Recommended)" "" on
932 Off "" off)
933 WebChoices=$("${WebToggleCommand[@]}" "${WebChooseOptions[@]}" 2>&1 >/dev/tty) || (echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}" && exit 1)
934 # Depending on their choice
935 case ${WebChoices} in
936 "On (Recommended)")
937 echo -e " ${INFO} Web Interface On"
938 # Set it to true
939 INSTALL_WEB=true
940 ;;
941 Off)
942 echo -e " ${INFO} Web Interface Off"
943 # or false
944 INSTALL_WEB=false
945 ;;
946 esac
947}
948
949# Check if /etc/dnsmasq.conf is from pihole. If so replace with an original and install new in .d directory
950version_check_dnsmasq() {
951 # Local, named variables
952 local dnsmasq_conf="/etc/dnsmasq.conf"
953 local dnsmasq_conf_orig="/etc/dnsmasq.conf.orig"
954 local dnsmasq_pihole_id_string="addn-hosts=/etc/pihole/gravity.list"
955 local dnsmasq_original_config="${PI_HOLE_LOCAL_REPO}/advanced/dnsmasq.conf.original"
956 local dnsmasq_pihole_01_snippet="${PI_HOLE_LOCAL_REPO}/advanced/01-pihole.conf"
957 local dnsmasq_pihole_01_location="/etc/dnsmasq.d/01-pihole.conf"
958
959 # If the dnsmasq config file exists
960 if [[ -f "${dnsmasq_conf}" ]]; then
961 echo -ne " ${INFO} Existing dnsmasq.conf found..."
962 # If gravity.list is found within this file, we presume it's from older versions on Pi-hole,
963 if grep -q ${dnsmasq_pihole_id_string} ${dnsmasq_conf}; then
964 echo " it is from a previous Pi-hole install."
965 echo -ne " ${INFO} Backing up dnsmasq.conf to dnsmasq.conf.orig..."
966 # so backup the original file
967 mv -f ${dnsmasq_conf} ${dnsmasq_conf_orig}
968 echo -e "${OVER} ${TICK} Backing up dnsmasq.conf to dnsmasq.conf.orig..."
969 echo -ne " ${INFO} Restoring default dnsmasq.conf..."
970 # and replace it with the default
971 cp ${dnsmasq_original_config} ${dnsmasq_conf}
972 echo -e "${OVER} ${TICK} Restoring default dnsmasq.conf..."
973 # Otherwise,
974 else
975 # Don't to anything
976 echo " it is not a Pi-hole file, leaving alone!"
977 fi
978 else
979 # If a file cannot be found,
980 echo -ne " ${INFO} No dnsmasq.conf found... restoring default dnsmasq.conf..."
981 # restore the default one
982 cp ${dnsmasq_original_config} ${dnsmasq_conf}
983 echo -e "${OVER} ${TICK} No dnsmasq.conf found... restoring default dnsmasq.conf..."
984 fi
985
986 echo -en " ${INFO} Copying 01-pihole.conf to /etc/dnsmasq.d/01-pihole.conf..."
987 # Copy the new Pi-hole DNS config file into the dnsmasq.d directory
988 cp ${dnsmasq_pihole_01_snippet} ${dnsmasq_pihole_01_location}
989 echo -e "${OVER} ${TICK} Copying 01-pihole.conf to /etc/dnsmasq.d/01-pihole.conf"
990 # Replace our placeholder values with the GLOBAL DNS variables that we populated earlier
991 # First, swap in the interface to listen on
992 sed -i "s/@INT@/$PIHOLE_INTERFACE/" ${dnsmasq_pihole_01_location}
993 if [[ "${PIHOLE_DNS_1}" != "" ]]; then
994 # Then swap in the primary DNS server
995 sed -i "s/@DNS1@/$PIHOLE_DNS_1/" ${dnsmasq_pihole_01_location}
996 else
997 #
998 sed -i '/^server=@DNS1@/d' ${dnsmasq_pihole_01_location}
999 fi
1000 if [[ "${PIHOLE_DNS_2}" != "" ]]; then
1001 # Then swap in the primary DNS server
1002 sed -i "s/@DNS2@/$PIHOLE_DNS_2/" ${dnsmasq_pihole_01_location}
1003 else
1004 #
1005 sed -i '/^server=@DNS2@/d' ${dnsmasq_pihole_01_location}
1006 fi
1007
1008 #
1009 sed -i 's/^#conf-dir=\/etc\/dnsmasq.d$/conf-dir=\/etc\/dnsmasq.d/' ${dnsmasq_conf}
1010
1011 # If the user does not want to enable logging,
1012 if [[ "${QUERY_LOGGING}" == false ]] ; then
1013 # Disable it by commenting out the directive in the DNS config file
1014 sed -i 's/^log-queries/#log-queries/' ${dnsmasq_pihole_01_location}
1015 # Otherwise,
1016 else
1017 # enable it by uncommenting the directive in the DNS config file
1018 sed -i 's/^#log-queries/log-queries/' ${dnsmasq_pihole_01_location}
1019 fi
1020}
1021
1022# Clean an existing installation to prepare for upgrade/reinstall
1023clean_existing() {
1024 # Local, named variables
1025 # ${1} Directory to clean
1026 local clean_directory="${1}"
1027 # Make ${2} the new one?
1028 shift
1029 # ${2} Array of files to remove
1030 local old_files=( "$@" )
1031
1032 # For each script found in the old files array
1033 for script in "${old_files[@]}"; do
1034 # Remove them
1035 rm -f "${clean_directory}/${script}.sh"
1036 done
1037}
1038
1039# Install the scripts from repository to their various locations
1040installScripts() {
1041 # Local, named variables
1042 local str="Installing scripts from ${PI_HOLE_LOCAL_REPO}"
1043 echo -ne " ${INFO} ${str}..."
1044
1045 # Clear out script files from Pi-hole scripts directory.
1046 clean_existing "${PI_HOLE_INSTALL_DIR}" "${PI_HOLE_FILES[@]}"
1047
1048 # Install files from local core repository
1049 if is_repo "${PI_HOLE_LOCAL_REPO}"; then
1050 # move into the directory
1051 cd "${PI_HOLE_LOCAL_REPO}"
1052 # Install the scripts by:
1053 # -o setting the owner to the user
1054 # -Dm755 create all leading components of destiantion except the last, then copy the source to the destiantion and setting the permissions to 755
1055 #
1056 # This first one is the directory
1057 install -o "${USER}" -Dm755 -d "${PI_HOLE_INSTALL_DIR}"
1058 # The rest are the scripts Pi-hole needs
1059 install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" gravity.sh
1060 install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/*.sh
1061 install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./automated\ install/uninstall.sh
1062 install -o "${USER}" -Dm755 -t "${PI_HOLE_INSTALL_DIR}" ./advanced/Scripts/COL_TABLE
1063 install -o "${USER}" -Dm755 -t /usr/local/bin/ pihole
1064 install -Dm644 ./advanced/bash-completion/pihole /etc/bash_completion.d/pihole
1065 echo -e "${OVER} ${TICK} ${str}"
1066 # Otherwise,
1067 else
1068 # Show an error and exit
1069 echo -e "${OVER} ${CROSS} ${str}
1070 ${COL_LIGHT_RED}Error: Local repo ${PI_HOLE_LOCAL_REPO} not found, exiting installer${COL_NC}"
1071 exit 1
1072 fi
1073}
1074
1075# Install the configs from PI_HOLE_LOCAL_REPO to their various locations
1076installConfigs() {
1077 echo ""
1078 echo -e " ${INFO} Installing configs from ${PI_HOLE_LOCAL_REPO}..."
1079 # Make sure Pi-hole's config files are in place
1080 version_check_dnsmasq
1081
1082 # If the user chose to install the dashboard,
1083 if [[ "${INSTALL_WEB}" == true ]]; then
1084 # and if the Web server conf directory does not exist,
1085 if [[ ! -d "/etc/lighttpd" ]]; then
1086 # make it
1087 mkdir /etc/lighttpd
1088 # and set the owners
1089 chown "${USER}":root /etc/lighttpd
1090 # Otherwise, if the config file already exists
1091 elif [[ -f "/etc/lighttpd/lighttpd.conf" ]]; then
1092 # back up the original
1093 mv /etc/lighttpd/lighttpd.conf /etc/lighttpd/lighttpd.conf.orig
1094 fi
1095 # and copy in the config file Pi-hole needs
1096 cp ${PI_HOLE_LOCAL_REPO}/advanced/${LIGHTTPD_CFG} /etc/lighttpd/lighttpd.conf
1097 # if there is a custom block page in the html/pihole directory, replace 404 handler in lighttpd config
1098 if [[ -f "/var/www/html/pihole/custom.php" ]]; then
1099 sed -i 's/^\(server\.error-handler-404\s*=\s*\).*$/\1"pihole\/custom\.php"/' /etc/lighttpd/lighttpd.conf
1100 fi
1101 # Make the directories if they do not exist and set the owners
1102 mkdir -p /var/run/lighttpd
1103 chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/run/lighttpd
1104 mkdir -p /var/cache/lighttpd/compress
1105 chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/compress
1106 mkdir -p /var/cache/lighttpd/uploads
1107 chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/cache/lighttpd/uploads
1108 fi
1109}
1110
1111stop_service() {
1112 # Stop service passed in as argument.
1113 # Can softfail, as process may not be installed when this is called
1114 local str="Stopping ${1} service"
1115 echo ""
1116 echo -ne " ${INFO} ${str}..."
1117 if command -v systemctl &> /dev/null; then
1118 systemctl stop "${1}" &> /dev/null || true
1119 else
1120 service "${1}" stop &> /dev/null || true
1121 fi
1122 echo -e "${OVER} ${TICK} ${str}..."
1123}
1124
1125# Start/Restart service passed in as argument
1126start_service() {
1127 # Local, named variables
1128 local str="Starting ${1} service"
1129 echo ""
1130 echo -ne " ${INFO} ${str}..."
1131 # If systemctl exists,
1132 if command -v systemctl &> /dev/null; then
1133 # use that to restart the service
1134 systemctl restart "${1}" &> /dev/null
1135 # Otherwise,
1136 else
1137 # fall back to the service command
1138 service "${1}" restart &> /dev/null
1139 fi
1140 echo -e "${OVER} ${TICK} ${str}"
1141}
1142
1143# Enable service so that it will start with next reboot
1144enable_service() {
1145 # Local, named variables
1146 local str="Enabling ${1} service to start on reboot"
1147 echo ""
1148 echo -ne " ${INFO} ${str}..."
1149 # If systemctl exists,
1150 if command -v systemctl &> /dev/null; then
1151 # use that to enable the service
1152 systemctl enable "${1}" &> /dev/null
1153 # Othwerwise,
1154 else
1155 # use update-rc.d to accomplish this
1156 update-rc.d "${1}" defaults &> /dev/null
1157 fi
1158 echo -e "${OVER} ${TICK} ${str}"
1159}
1160
1161update_package_cache() {
1162 # Running apt-get update/upgrade with minimal output can cause some issues with
1163 # requiring user input (e.g password for phpmyadmin see #218)
1164
1165 # Update package cache on apt based OSes. Do this every time since
1166 # it's quick and packages can be updated at any time.
1167
1168 # Local, named variables
1169 local str="Update local cache of available packages"
1170 echo ""
1171 echo -ne " ${INFO} ${str}..."
1172 # Create a command from the package cache variable
1173 if eval "${UPDATE_PKG_CACHE}" &> /dev/null; then
1174 echo -e "${OVER} ${TICK} ${str}"
1175 # Otherwise,
1176 else
1177 # show an error and exit
1178 echo -e "${OVER} ${CROSS} ${str}"
1179 echo -ne " ${COL_LIGHT_RED}Error: Unable to update package cache. Please try \"${UPDATE_PKG_CACHE}\"${COL_NC}"
1180 return 1
1181 fi
1182}
1183
1184# Let user know if they have outdated packages on their system and
1185# advise them to run a package update at soonest possible.
1186notify_package_updates_available() {
1187 # Local, named variables
1188 local str="Checking ${PKG_MANAGER} for upgraded packages"
1189 echo -ne "\\n ${INFO} ${str}..."
1190 # Store the list of packages in a variable
1191 updatesToInstall=$(eval "${PKG_COUNT}")
1192
1193 if [[ -d "/lib/modules/$(uname -r)" ]]; then
1194 #
1195 if [[ "${updatesToInstall}" -eq 0 ]]; then
1196 #
1197 echo -e "${OVER} ${TICK} ${str}... up to date!"
1198 echo ""
1199 else
1200 #
1201 echo -e "${OVER} ${TICK} ${str}... ${updatesToInstall} updates available"
1202 echo -e " ${INFO} ${COL_LIGHT_GREEN}It is recommended to update your OS after installing the Pi-hole! ${COL_NC}"
1203 echo ""
1204 fi
1205 else
1206 echo -e "${OVER} ${CROSS} ${str}
1207 Kernel update detected. If the install fails, please reboot and try again\\n"
1208 fi
1209}
1210
1211# What's this doing outside of a function in the middle of nowhere?
1212counter=0
1213
1214install_dependent_packages() {
1215 # Local, named variables should be used here, especially for an iterator
1216 # Add one to the counter
1217 counter=$((counter+1))
1218 # If it equals 1,
1219 if [[ "${counter}" == 1 ]]; then
1220 #
1221 echo -e " ${INFO} Installer Dependency checks..."
1222 else
1223 #
1224 echo -e " ${INFO} Main Dependency checks..."
1225 fi
1226
1227 # Install packages passed in via argument array
1228 # No spinner - conflicts with set -e
1229 declare -a argArray1=("${!1}")
1230 declare -a installArray
1231
1232 # Debian based package install - debconf will download the entire package list
1233 # so we just create an array of packages not currently installed to cut down on the
1234 # amount of download traffic.
1235 # NOTE: We may be able to use this installArray in the future to create a list of package that were
1236 # installed by us, and remove only the installed packages, and not the entire list.
1237 if command -v debconf-apt-progress &> /dev/null; then
1238 # For each package,
1239 for i in "${argArray1[@]}"; do
1240 echo -ne " ${INFO} Checking for $i..."
1241 #
1242 if dpkg-query -W -f='${Status}' "${i}" 2>/dev/null | grep "ok installed" &> /dev/null; then
1243 #
1244 echo -e "${OVER} ${TICK} Checking for $i"
1245 else
1246 #
1247 echo -e "${OVER} ${INFO} Checking for $i (will be installed)"
1248 #
1249 installArray+=("${i}")
1250 fi
1251 done
1252 #
1253 if [[ "${#installArray[@]}" -gt 0 ]]; then
1254 #
1255 test_dpkg_lock
1256 #
1257 debconf-apt-progress -- "${PKG_INSTALL[@]}" "${installArray[@]}"
1258 return
1259 fi
1260 echo ""
1261 #
1262 return 0
1263 fi
1264
1265 # Install Fedora/CentOS packages
1266 for i in "${argArray1[@]}"; do
1267 echo -ne " ${INFO} Checking for $i..."
1268 #
1269 if ${PKG_MANAGER} -q list installed "${i}" &> /dev/null; then
1270 echo -e "${OVER} ${TICK} Checking for $i"
1271 else
1272 echo -e "${OVER} ${INFO} Checking for $i (will be installed)"
1273 #
1274 installArray+=("${i}")
1275 fi
1276 done
1277 #
1278 if [[ "${#installArray[@]}" -gt 0 ]]; then
1279 #
1280 "${PKG_INSTALL[@]}" "${installArray[@]}" &> /dev/null
1281 return
1282 fi
1283 echo ""
1284 return 0
1285}
1286
1287# Create logfiles if necessary
1288CreateLogFile() {
1289 local str="Creating log and changing owner to dnsmasq"
1290 echo ""
1291 echo -ne " ${INFO} ${str}..."
1292 # If the pihole log does not exist,
1293 if [[ ! -f "/var/log/pihole.log" ]]; then
1294 # Make it,
1295 touch /var/log/pihole.log
1296 # set the permissions,
1297 chmod 644 /var/log/pihole.log
1298 # and owners
1299 chown "${DNSMASQ_USER}":root /var/log/pihole.log
1300 echo -e "${OVER} ${TICK} ${str}"
1301 # Otherwise,
1302 else
1303 # the file should already exist
1304 echo -e " ${COL_LIGHT_GREEN}log already exists!${COL_NC}"
1305 fi
1306}
1307
1308# Install the Web interface dashboard
1309installPiholeWeb() {
1310 echo ""
1311 echo " ${INFO} Installing blocking page..."
1312
1313 local str="Creating directory for blocking page, and copying files"
1314 echo -ne " ${INFO} ${str}..."
1315 # Install the directory
1316 install -d /var/www/html/pihole
1317 # and the blockpage
1318 install -D ${PI_HOLE_LOCAL_REPO}/advanced/{index,blockingpage}.* /var/www/html/pihole/
1319
1320 # Remove superseded file
1321 if [[ -e "/var/www/html/pihole/index.js" ]]; then
1322 rm "/var/www/html/pihole/index.js"
1323 fi
1324
1325 echo -e "${OVER} ${TICK} ${str}"
1326
1327 local str="Backing up index.lighttpd.html"
1328 echo -ne " ${INFO} ${str}..."
1329 # If the default index file exists,
1330 if [[ -f "/var/www/html/index.lighttpd.html" ]]; then
1331 # back it up
1332 mv /var/www/html/index.lighttpd.html /var/www/html/index.lighttpd.orig
1333 echo -e "${OVER} ${TICK} ${str}"
1334 # Othwerwise,
1335 else
1336 # don't do anything
1337 echo -e "${OVER} ${CROSS} ${str}
1338 No default index.lighttpd.html file found... not backing up"
1339 fi
1340
1341 # Install Sudoers file
1342 echo ""
1343 local str="Installing sudoer file"
1344 echo -ne " ${INFO} ${str}..."
1345 # Make the .d directory if it doesn't exist
1346 mkdir -p /etc/sudoers.d/
1347 # and copy in the pihole sudoers file
1348 cp ${PI_HOLE_LOCAL_REPO}/advanced/pihole.sudo /etc/sudoers.d/pihole
1349 # Add lighttpd user (OS dependent) to sudoers file
1350 echo "${LIGHTTPD_USER} ALL=NOPASSWD: /usr/local/bin/pihole" >> /etc/sudoers.d/pihole
1351
1352 # If the Web server user is lighttpd,
1353 if [[ "$LIGHTTPD_USER" == "lighttpd" ]]; then
1354 # Allow executing pihole via sudo with Fedora
1355 # Usually /usr/local/bin is not permitted as directory for sudoable programms
1356 echo "Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin" >> /etc/sudoers.d/pihole
1357 fi
1358 # Set the strict permissions on the file
1359 chmod 0440 /etc/sudoers.d/pihole
1360 echo -e "${OVER} ${TICK} ${str}"
1361}
1362
1363# Installs a cron file
1364installCron() {
1365 # Install the cron job
1366 local str="Installing latest Cron script"
1367 echo ""
1368 echo -ne " ${INFO} ${str}..."
1369 # Copy the cron file over from the local repo
1370 cp ${PI_HOLE_LOCAL_REPO}/advanced/pihole.cron /etc/cron.d/pihole
1371 # Randomize gravity update time
1372 sed -i "s/59 1 /$((1 + RANDOM % 58)) $((3 + RANDOM % 2))/" /etc/cron.d/pihole
1373 # Randomize update checker time
1374 sed -i "s/59 17/$((1 + RANDOM % 58)) $((12 + RANDOM % 8))/" /etc/cron.d/pihole
1375 echo -e "${OVER} ${TICK} ${str}"
1376}
1377
1378# Gravity is a very important script as it aggregates all of the domains into a single HOSTS formatted list,
1379# which is what Pi-hole needs to begin blocking ads
1380runGravity() {
1381 echo ""
1382 echo -e " ${INFO} Preparing to run gravity.sh to refresh hosts..."
1383 # If cached lists exist,
1384 if ls /etc/pihole/list* 1> /dev/null 2>&1; then
1385 echo -e " ${INFO} Cleaning up previous install (preserving whitelist/blacklist)"
1386 # remove them
1387 rm /etc/pihole/list.*
1388 fi
1389 # If the default ad lists file exists,
1390 if [[ ! -e /etc/pihole/adlists.default ]]; then
1391 # copy it over from the local repo
1392 cp ${PI_HOLE_LOCAL_REPO}/adlists.default /etc/pihole/adlists.default
1393 fi
1394 echo -e " ${INFO} Running gravity.sh"
1395 # Run gravity in the current shell
1396 { /opt/pihole/gravity.sh; }
1397}
1398
1399# Check if the pihole user exists and create if it does not
1400create_pihole_user() {
1401 local str="Checking for user 'pihole'"
1402 echo -ne " ${INFO} ${str}..."
1403 # If the user pihole exists,
1404 if id -u pihole &> /dev/null; then
1405 # just show a success
1406 echo -ne "${OVER} ${TICK} ${str}"
1407 # Othwerwise,
1408 else
1409 echo -ne "${OVER} ${CROSS} ${str}"
1410 local str="Creating user 'pihole'"
1411 echo -ne " ${INFO} ${str}..."
1412 # create her with the useradd command
1413 useradd -r -s /usr/sbin/nologin pihole
1414 echo -ne "${OVER} ${TICK} ${str}"
1415 fi
1416}
1417
1418# Allow HTTP and DNS traffic
1419configureFirewall() {
1420 echo ""
1421 # If a firewall is running,
1422 if firewall-cmd --state &> /dev/null; then
1423 # ask if the user wants to install Pi-hole's default firwall rules
1424 whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" ${r} ${c} || \
1425 { echo -e " ${INFO} Not installing firewall rulesets."; return 0; }
1426 echo -e " ${TICK} Configuring FirewallD for httpd and dnsmasq"
1427 # Allow HTTP and DNS traffice
1428 firewall-cmd --permanent --add-service=http --add-service=dns
1429 # Reload the firewall to apply these changes
1430 firewall-cmd --reload
1431 return 0
1432 # Check for proper kernel modules to prevent failure
1433 elif modinfo ip_tables &> /dev/null && command -v iptables &> /dev/null; then
1434 # If chain Policy is not ACCEPT or last Rule is not ACCEPT
1435 # then check and insert our Rules above the DROP/REJECT Rule.
1436 if iptables -S INPUT | head -n1 | grep -qv '^-P.*ACCEPT$' || iptables -S INPUT | tail -n1 | grep -qv '^-\(A\|P\).*ACCEPT$'; then
1437 whiptail --title "Firewall in use" --yesno "We have detected a running firewall\\n\\nPi-hole currently requires HTTP and DNS port access.\\n\\n\\n\\nInstall Pi-hole default firewall rules?" ${r} ${c} || \
1438 { echo -e " ${INFO} Not installing firewall rulesets."; return 0; }
1439 echo -e " ${TICK} Installing new IPTables firewall rulesets"
1440 # Check chain first, otherwise a new rule will duplicate old ones
1441 iptables -C INPUT -p tcp -m tcp --dport 80 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p tcp -m tcp --dport 80 -j ACCEPT
1442 iptables -C INPUT -p tcp -m tcp --dport 53 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p tcp -m tcp --dport 53 -j ACCEPT
1443 iptables -C INPUT -p udp -m udp --dport 53 -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p udp -m udp --dport 53 -j ACCEPT
1444 iptables -C INPUT -p tcp -m tcp --dport 4711:4720 -i lo -j ACCEPT &> /dev/null || iptables -I INPUT 1 -p tcp -m tcp --dport 4711:4720 -i lo -j ACCEPT
1445 return 0
1446 fi
1447 # Othwerwise,
1448 else
1449 # no firewall is running
1450 echo -e " ${INFO} No active firewall detected.. skipping firewall configuration"
1451 # so just exit
1452 return 0
1453 fi
1454 echo -e " ${INFO} Skipping firewall configuration"
1455}
1456
1457#
1458finalExports() {
1459 # If the Web interface is not set to be installed,
1460 if [[ "${INSTALL_WEB}" == false ]]; then
1461 # and if there is not an IPv4 address,
1462 if [[ "${IPV4_ADDRESS}" ]]; then
1463 # there is no block page, so set IPv4 to 0.0.0.0 (all IP addresses)
1464 IPV4_ADDRESS="0.0.0.0"
1465 fi
1466 if [[ "${IPV6_ADDRESS}" ]]; then
1467 # and IPv6 to ::/0
1468 IPV6_ADDRESS="::/0"
1469 fi
1470 fi
1471
1472 # If the setup variable file exists,
1473 if [[ -e "${setupVars}" ]]; then
1474 # update the variables in the file
1475 sed -i.update.bak '/PIHOLE_INTERFACE/d;/IPV4_ADDRESS/d;/IPV6_ADDRESS/d;/PIHOLE_DNS_1/d;/PIHOLE_DNS_2/d;/QUERY_LOGGING/d;/INSTALL_WEB/d;/LIGHTTPD_ENABLED/d;' "${setupVars}"
1476 fi
1477 # echo the information to the user
1478 {
1479 echo "PIHOLE_INTERFACE=${PIHOLE_INTERFACE}"
1480 echo "IPV4_ADDRESS=${IPV4_ADDRESS}"
1481 echo "IPV6_ADDRESS=${IPV6_ADDRESS}"
1482 echo "PIHOLE_DNS_1=${PIHOLE_DNS_1}"
1483 echo "PIHOLE_DNS_2=${PIHOLE_DNS_2}"
1484 echo "QUERY_LOGGING=${QUERY_LOGGING}"
1485 echo "INSTALL_WEB=${INSTALL_WEB}"
1486 echo "LIGHTTPD_ENABLED=${LIGHTTPD_ENABLED}"
1487 }>> "${setupVars}"
1488
1489 # Bring in the current settings and the functions to manipulate them
1490 source "${setupVars}"
1491 source "${PI_HOLE_LOCAL_REPO}/advanced/Scripts/webpage.sh"
1492
1493 # Look for DNS server settings which would have to be reapplied
1494 ProcessDNSSettings
1495
1496 # Look for DHCP server settings which would have to be reapplied
1497 ProcessDHCPSettings
1498}
1499
1500# Install the logrotate script
1501installLogrotate() {
1502
1503 local str="Installing latest logrotate script"
1504 echo ""
1505 echo -ne " ${INFO} ${str}..."
1506 # Copy the file over from the local repo
1507 cp ${PI_HOLE_LOCAL_REPO}/advanced/logrotate /etc/pihole/logrotate
1508 # Different operating systems have different user / group
1509 # settings for logrotate that makes it impossible to create
1510 # a static logrotate file that will work with e.g.
1511 # Rasbian and Ubuntu at the same time. Hence, we have to
1512 # customize the logrotate script here in order to reflect
1513 # the local properties of the /var/log directory
1514 logusergroup="$(stat -c '%U %G' /var/log)"
1515 # If the variable has a value,
1516 if [[ ! -z "${logusergroup}" ]]; then
1517 #
1518 sed -i "s/# su #/su ${logusergroup}/g;" /etc/pihole/logrotate
1519 fi
1520 echo -e "${OVER} ${TICK} ${str}"
1521}
1522
1523# Install base files and web interface
1524installPihole() {
1525 # Create the pihole user
1526 create_pihole_user
1527
1528 # If the user wants to install the Web interface,
1529 if [[ "${INSTALL_WEB}" == true ]]; then
1530 if [[ ! -d "/var/www/html" ]]; then
1531 # make the Web directory if necessary
1532 mkdir -p /var/www/html
1533 fi
1534 # Set the owner and permissions
1535 chown ${LIGHTTPD_USER}:${LIGHTTPD_GROUP} /var/www/html
1536 chmod 775 /var/www/html
1537 # Give pihole access to the Web server group
1538 usermod -a -G ${LIGHTTPD_GROUP} pihole
1539 # If the lighttpd command is executable,
1540 if [[ -x "$(command -v lighty-enable-mod)" ]]; then
1541 # enable fastcgi and fastcgi-php
1542 lighty-enable-mod fastcgi fastcgi-php > /dev/null || true
1543 else
1544 # Othweise, show info about installing them
1545 echo -e " ${INFO} Warning: 'lighty-enable-mod' utility not found
1546 Please ensure fastcgi is enabled if you experience issues\\n"
1547 fi
1548 fi
1549 # Install scripts,
1550 installScripts
1551 # configs,
1552 installConfigs
1553 # and create the log file
1554 CreateLogFile
1555 # If the user wants to install the dashboard,
1556 if [[ "${INSTALL_WEB}" == true ]]; then
1557 # do so
1558 installPiholeWeb
1559 fi
1560 # Install the cron file
1561 installCron
1562 # Install the logrotate file
1563 installLogrotate
1564 # Check if FTL is installed
1565 FTLdetect || echo -e " ${CROSS} FTL Engine not installed"
1566 # Configure the firewall
1567 configureFirewall
1568
1569 #update setupvars.conf with any variables that may or may not have been changed during the install
1570 finalExports
1571}
1572
1573# At some point in the future this list can be pruned, for now we'll need it to ensure updates don't break.
1574# Refactoring of install script has changed the name of a couple of variables. Sort them out here.
1575accountForRefactor() {
1576 sed -i 's/piholeInterface/PIHOLE_INTERFACE/g' ${setupVars}
1577 sed -i 's/IPv4_address/IPV4_ADDRESS/g' ${setupVars}
1578 sed -i 's/IPv4addr/IPV4_ADDRESS/g' ${setupVars}
1579 sed -i 's/IPv6_address/IPV6_ADDRESS/g' ${setupVars}
1580 sed -i 's/piholeIPv6/IPV6_ADDRESS/g' ${setupVars}
1581 sed -i 's/piholeDNS1/PIHOLE_DNS_1/g' ${setupVars}
1582 sed -i 's/piholeDNS2/PIHOLE_DNS_2/g' ${setupVars}
1583}
1584
1585updatePihole() {
1586 accountForRefactor
1587 # Install base files and web interface
1588 installScripts
1589 # Install config files
1590 installConfigs
1591 # Create the log file
1592 CreateLogFile
1593 # If the user wants to install the dasboard,
1594 if [[ "${INSTALL_WEB}" == true ]]; then
1595 # do so
1596 installPiholeWeb
1597 fi
1598 # Install the cron file
1599 installCron
1600 # Install logrotate
1601 installLogrotate
1602 # Detect if FTL is installed
1603 FTLdetect || echo -e " ${CROSS} FTL Engine not installed."
1604
1605 #update setupvars.conf with any variables that may or may not have been changed during the install
1606 finalExports
1607
1608}
1609
1610
1611# SELinux
1612checkSelinux() {
1613 # If the getenforce command exists,
1614 if command -v getenforce &> /dev/null; then
1615 # Store the current mode in a variable
1616 enforceMode=$(getenforce)
1617 echo -e "\\n ${INFO} SELinux mode detected: ${enforceMode}"
1618
1619 # If it's enforcing,
1620 if [[ "${enforceMode}" == "Enforcing" ]]; then
1621 # Explain Pi-hole does not support it yet
1622 whiptail --defaultno --title "SELinux Enforcing Detected" --yesno "SELinux is being ENFORCED on your system! \\n\\nPi-hole currently does not support SELinux, but you may still continue with the installation.\\n\\nNote: Web Admin will not be fully functional unless you set your policies correctly\\n\\nContinue installing Pi-hole?" ${r} ${c} || \
1623 { echo -e "\\n ${COL_LIGHT_RED}SELinux Enforcing detected, exiting installer${COL_NC}"; exit 1; }
1624 echo -e " ${INFO} Continuing installation with SELinux Enforcing
1625 ${INFO} Please refer to official SELinux documentation to create a custom policy"
1626 fi
1627 fi
1628}
1629
1630# Installation complete message with instructions for the user
1631displayFinalMessage() {
1632 # If
1633 if [[ "${#1}" -gt 0 ]] ; then
1634 pwstring="$1"
1635 # else, if the dashboard password in the setup variables exists,
1636 elif [[ $(grep 'WEBPASSWORD' -c /etc/pihole/setupVars.conf) -gt 0 ]]; then
1637 # set a variable for evaluation later
1638 pwstring="unchanged"
1639 else
1640 # set a variable for evaluation later
1641 pwstring="NOT SET"
1642 fi
1643 # If the user wants to install the dashboard,
1644 if [[ "${INSTALL_WEB}" == true ]]; then
1645 # Store a message in a variable and display it
1646 additional="View the web interface at http://pi.hole/admin or http://${IPV4_ADDRESS%/*}/admin
1647
1648Your Admin Webpage login password is ${pwstring}"
1649 fi
1650
1651 # Final completion message to user
1652 whiptail --msgbox --backtitle "Make it so." --title "Installation Complete!" "Configure your devices to use the Pi-hole as their DNS server using:
1653
1654IPv4: ${IPV4_ADDRESS%/*}
1655IPv6: ${IPV6_ADDRESS:-"Not Configured"}
1656
1657If you set a new IP address, you should restart the Pi.
1658
1659The install log is in /etc/pihole.
1660
1661${additional}" ${r} ${c}
1662}
1663
1664update_dialogs() {
1665 # If pihole -r "reconfigure" option was selected,
1666 if [[ "${reconfigure}" = true ]]; then
1667 # set some variables that will be used
1668 opt1a="Repair"
1669 opt1b="This will retain existing settings"
1670 strAdd="You will remain on the same version"
1671 # Othweise,
1672 else
1673 # set some variables with different values
1674 opt1a="Update"
1675 opt1b="This will retain existing settings."
1676 strAdd="You will be updated to the latest version."
1677 fi
1678 opt2a="Reconfigure"
1679 opt2b="This will allow you to enter new settings"
1680
1681 # Display the information to the user
1682 UpdateCmd=$(whiptail --title "Existing Install Detected!" --menu "\\n\\nWe have detected an existing install.\\n\\nPlease choose from the following options: \\n($strAdd)" ${r} ${c} 2 \
1683 "${opt1a}" "${opt1b}" \
1684 "${opt2a}" "${opt2b}" 3>&2 2>&1 1>&3) || \
1685 { echo -e " ${COL_LIGHT_RED}Cancel was selected, exiting installer${COL_NC}"; exit 1; }
1686
1687 # Set the variable based on if the user chooses
1688 case ${UpdateCmd} in
1689 # repair, or
1690 ${opt1a})
1691 echo -e " ${INFO} ${opt1a} option selected"
1692 useUpdateVars=true
1693 ;;
1694 # reconfigure,
1695 ${opt2a})
1696 echo -e " ${INFO} ${opt2a} option selected"
1697 useUpdateVars=false
1698 ;;
1699 esac
1700}
1701
1702clone_or_update_repos() {
1703 # If the user wants to reconfigure,
1704 if [[ "${reconfigure}" == true ]]; then
1705 echo " ${INFO} Performing reconfiguration, skipping download of local repos"
1706 # Reset the Core repo
1707 resetRepo ${PI_HOLE_LOCAL_REPO} || \
1708 { echo -e " ${COL_LIGHT_RED}Unable to reset ${PI_HOLE_LOCAL_REPO}, exiting installer${COL_NC}"; \
1709 exit 1; \
1710 }
1711 # If the Web interface was installed,
1712 if [[ "${INSTALL_WEB}" == true ]]; then
1713 # reset it's repo
1714 resetRepo ${webInterfaceDir} || \
1715 { echo -e " ${COL_LIGHT_RED}Unable to reset ${webInterfaceDir}, exiting installer${COL_NC}"; \
1716 exit 1; \
1717 }
1718 fi
1719 # Otherwise, a repair is happening
1720 else
1721 # so get git files for Core
1722 getGitFiles ${PI_HOLE_LOCAL_REPO} ${piholeGitUrl} || \
1723 { echo -e " ${COL_LIGHT_RED}Unable to clone ${piholeGitUrl} into ${PI_HOLE_LOCAL_REPO}, unable to continue${COL_NC}"; \
1724 exit 1; \
1725 }
1726 # If the Web interface was installed,
1727 if [[ "${INSTALL_WEB}" == true ]]; then
1728 # get the Web git files
1729 getGitFiles ${webInterfaceDir} ${webInterfaceGitUrl} || \
1730 { echo -e " ${COL_LIGHT_RED}Unable to clone ${webInterfaceGitUrl} into ${webInterfaceDir}, exiting installer${COL_NC}"; \
1731 exit 1; \
1732 }
1733 fi
1734 fi
1735}
1736
1737# Download and install FTL binary
1738FTLinstall() {
1739 # Local, named variables
1740 local binary="${1}"
1741 local latesttag
1742 local orig_dir
1743 local str="Downloading and Installing FTL"
1744 echo -ne " ${INFO} ${str}..."
1745
1746 # Get the current working directory
1747 orig_dir="${PWD}"
1748 # Find the latest version tag for FTL
1749 latesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep "Location" | awk -F '/' '{print $NF}')
1750 # Tags should always start with v, check for that.
1751 if [[ ! "${latesttag}" == v* ]]; then
1752 echo -e "${OVER} ${CROSS} ${str}"
1753 echo -e " ${COL_LIGHT_RED}Error: Unable to get latest release location from GitHub${COL_NC}"
1754 return 1
1755 fi
1756
1757 # If the download worked,
1758 if curl -sSL --fail "https://github.com/pi-hole/FTL/releases/download/${latesttag%$'\r'}/${binary}" -o "/tmp/${binary}"; then
1759 # get sha1 of the binary we just downloaded for verification.
1760 curl -sSL --fail "https://github.com/pi-hole/FTL/releases/download/${latesttag%$'\r'}/${binary}.sha1" -o "/tmp/${binary}.sha1"
1761
1762 # Move into the temp directory
1763 cd /tmp
1764 # If we downloaded binary file (as opposed to text),
1765 if sha1sum --status --quiet -c "${binary}".sha1; then
1766 echo -n "transferred... "
1767 # Stop FTL
1768 stop_service pihole-FTL &> /dev/null
1769 # Install the new version with the correct permissions
1770 install -T -m 0755 /tmp/${binary} /usr/bin/pihole-FTL
1771 # Remove the tempoary file
1772 rm /tmp/${binary} /tmp/${binary}.sha1
1773 # Move back into the original directory the user was in
1774 cd "${orig_dir}"
1775 # Install the FTL service
1776 install -T -m 0755 "${PI_HOLE_LOCAL_REPO}/advanced/pihole-FTL.service" "/etc/init.d/pihole-FTL"
1777 echo -e "${OVER} ${TICK} ${str}"
1778 return 0
1779 # Otherise,
1780 else
1781 echo -e "${OVER} ${CROSS} ${str}"
1782 echo -e " ${COL_LIGHT_RED}Error: Download of binary from Github failed${COL_NC}"
1783 # the download failed, so just go back to the original directory
1784 cd "${orig_dir}"
1785 return 1
1786 fi
1787 # Otherwise,
1788 else
1789 cd "${orig_dir}"
1790 echo -e "${OVER} ${CROSS} ${str}"
1791 # The URL could not be found
1792 echo -e " ${COL_LIGHT_RED}Error: URL not found${COL_NC}"
1793 fi
1794}
1795
1796# Detect suitable FTL binary platform
1797FTLdetect() {
1798 echo ""
1799 echo -e " ${INFO} FTL Checks..."
1800
1801 # Local, named variables
1802 local machine
1803 local binary
1804
1805 # Store architecture in a variable
1806 machine=$(uname -m)
1807
1808 local str="Detecting architecture"
1809 echo -ne " ${INFO} ${str}..."
1810 # If the machine is arm or aarch
1811 if [[ "${machine}" == "arm"* || "${machine}" == *"aarch"* ]]; then
1812 # ARM
1813 #
1814 local rev
1815 rev=$(uname -m | sed "s/[^0-9]//g;")
1816 #
1817 local lib
1818 lib=$(ldd /bin/ls | grep -E '^\s*/lib' | awk '{ print $1 }')
1819 #
1820 if [[ "${lib}" == "/lib/ld-linux-aarch64.so.1" ]]; then
1821 echo -e "${OVER} ${TICK} Detected ARM-aarch64 architecture"
1822 # set the binary to be used
1823 binary="pihole-FTL-aarch64-linux-gnu"
1824 #
1825 elif [[ "${lib}" == "/lib/ld-linux-armhf.so.3" ]]; then
1826 #
1827 if [[ "${rev}" -gt 6 ]]; then
1828 echo -e "${OVER} ${TICK} Detected ARM-hf architecture (armv7+)"
1829 # set the binary to be used
1830 binary="pihole-FTL-arm-linux-gnueabihf"
1831 # Otherwise,
1832 else
1833 echo -e "${OVER} ${TICK} Detected ARM-hf architecture (armv6 or lower) Using ARM binary"
1834 # set the binary to be used
1835 binary="pihole-FTL-arm-linux-gnueabi"
1836 fi
1837 else
1838 echo -e "${OVER} ${TICK} Detected ARM architecture"
1839 # set the binary to be used
1840 binary="pihole-FTL-arm-linux-gnueabi"
1841 fi
1842 elif [[ "${machine}" == "ppc" ]]; then
1843 # PowerPC
1844 echo -e "${OVER} ${TICK} Detected PowerPC architecture"
1845 # set the binary to be used
1846 binary="pihole-FTL-powerpc-linux-gnu"
1847 elif [[ "${machine}" == "x86_64" ]]; then
1848 # 64bit
1849 echo -e "${OVER} ${TICK} Detected x86_64 architecture"
1850 # set the binary to be used
1851 binary="pihole-FTL-linux-x86_64"
1852 else
1853 # Something else - we try to use 32bit executable and warn the user
1854 if [[ ! "${machine}" == "i686" ]]; then
1855 echo -e "${OVER} ${CROSS} ${str}...
1856 ${COL_LIGHT_RED}Not able to detect architecture (unknown: ${machine}), trying 32bit executable${COL_NC}
1857 Contact Pi-hole Support if you experience issues (e.g: FTL not running)"
1858 else
1859 echo -e "${OVER} ${TICK} Detected 32bit (i686) architecture"
1860 fi
1861 binary="pihole-FTL-linux-x86_32"
1862 fi
1863
1864 #In the next section we check to see if FTL is already installed (in case of pihole -r).
1865 #If the installed version matches the latest version, then check the installed sha1sum of the binary vs the remote sha1sum. If they do not match, then download
1866 echo -e " ${INFO} Checking for existing FTL binary..."
1867
1868 local ftlLoc=$(which pihole-FTL 2>/dev/null)
1869
1870 if [[ ${ftlLoc} ]]; then
1871 local FTLversion=$(/usr/bin/pihole-FTL tag)
1872 local FTLlatesttag=$(curl -sI https://github.com/pi-hole/FTL/releases/latest | grep 'Location' | awk -F '/' '{print $NF}' | tr -d '\r\n')
1873
1874 if [[ "${FTLversion}" != "${FTLlatesttag}" ]]; then
1875 # Install FTL
1876 FTLinstall "${binary}" || return 1
1877 else
1878 echo -e " ${INFO} Latest FTL Binary already installed (${FTLlatesttag}). Confirming Checksum..."
1879
1880 local remoteSha1=$(curl -sSL --fail "https://github.com/pi-hole/FTL/releases/download/${FTLversion%$'\r'}/${binary}.sha1" | cut -d ' ' -f 1)
1881 local localSha1=$(sha1sum "$(which pihole-FTL)" | cut -d ' ' -f 1)
1882
1883 if [[ "${remoteSha1}" != "${localSha1}" ]]; then
1884 echo -e " ${INFO} Corruption detected..."
1885 FTLinstall "${binary}" || return 1
1886 else
1887 echo -e " ${INFO} Checksum correct. No need to download!"
1888 fi
1889 fi
1890 else
1891 # Install FTL
1892 FTLinstall "${binary}" || return 1
1893 fi
1894}
1895
1896make_temporary_log() {
1897 # Create a random temporary file for the log
1898 TEMPLOG=$(mktemp /tmp/pihole_temp.XXXXXX)
1899 # Open handle 3 for templog
1900 # https://stackoverflow.com/questions/18460186/writing-outputs-to-log-file-and-console
1901 exec 3>"$TEMPLOG"
1902 # Delete templog, but allow for addressing via file handle
1903 # This lets us write to the log without having a temporary file on the drive, which
1904 # is meant to be a security measure so there is not a lingering file on the drive during the install process
1905 rm "$TEMPLOG"
1906}
1907
1908copy_to_install_log() {
1909 # Copy the contents of file descriptor 3 into the install log
1910 # Since we use color codes such as '\e[1;33m', they should be removed
1911 sed 's/\[[0-9;]\{1,5\}m//g' < /proc/$$/fd/3 > "${installLogLoc}"
1912}
1913
1914main() {
1915 ######## FIRST CHECK ########
1916 # Must be root to install
1917 local str="Root user check"
1918 echo ""
1919
1920 # If the user's id is zero,
1921 if [[ "${EUID}" -eq 0 ]]; then
1922 # they are root and all is good
1923 echo -e " ${TICK} ${str}"
1924 # Show the Pi-hole logo so people know it's genuine since the logo and name are trademarked
1925 show_ascii_berry
1926 make_temporary_log
1927 # Otherwise,
1928 else
1929 # They do not have enough privileges, so let the user know
1930 echo -e " ${CROSS} ${str}
1931 ${COL_LIGHT_RED}Script called with non-root privileges${COL_NC}
1932 The Pi-hole requires elevated privileges to install and run
1933 Please check the installer for any concerns regarding this requirement
1934 Make sure to download this script from a trusted source\\n"
1935 echo -ne " ${INFO} Sudo utility check"
1936
1937 # If the sudo command exists,
1938 if command -v sudo &> /dev/null; then
1939 echo -e "${OVER} ${TICK} Sudo utility check"
1940 # Download the install script and run it with admin rights
1941 exec curl -sSL https://raw.githubusercontent.com/pi-hole/pi-hole/master/automated%20install/basic-install.sh | sudo bash "$@"
1942 exit $?
1943 # Otherwise,
1944 else
1945 # Let them know they need to run it as root
1946 echo -e "${OVER} ${CROSS} Sudo utility check
1947 Sudo is needed for the Web Interface to run pihole commands\\n
1948 ${COL_LIGHT_RED}Please re-run this installer as root${COL_NC}"
1949 exit 1
1950 fi
1951 fi
1952
1953 # Check for supported distribution
1954 distro_check
1955
1956 # Check arguments for the undocumented flags
1957 for var in "$@"; do
1958 case "$var" in
1959 "--reconfigure" ) reconfigure=true;;
1960 "--i_do_not_follow_recommendations" ) skipSpaceCheck=false;;
1961 "--unattended" ) runUnattended=true;;
1962 esac
1963 done
1964
1965 # If the setup variable file exists,
1966 if [[ -f "${setupVars}" ]]; then
1967 # if it's running unattended,
1968 if [[ "${runUnattended}" == true ]]; then
1969 echo -e " ${INFO} Performing unattended setup, no whiptail dialogs will be displayed"
1970 # Use the setup variables
1971 useUpdateVars=true
1972 # Otherwise,
1973 else
1974 # show the available options (repair/reconfigure)
1975 update_dialogs
1976 fi
1977 fi
1978
1979 # Start the installer
1980 # Verify there is enough disk space for the install
1981 if [[ "${skipSpaceCheck}" == true ]]; then
1982 echo -e " ${INFO} Skipping free disk space verification"
1983 else
1984 verifyFreeDiskSpace
1985 fi
1986
1987 # Update package cache
1988 update_package_cache || exit 1
1989
1990 # Notify user of package availability
1991 notify_package_updates_available
1992
1993 # Install packages used by this installation script
1994 install_dependent_packages INSTALLER_DEPS[@]
1995
1996 # Check if SELinux is Enforcing
1997 checkSelinux
1998
1999 if [[ "${useUpdateVars}" == false ]]; then
2000 # Display welcome dialogs
2001 welcomeDialogs
2002 # Create directory for Pi-hole storage
2003 mkdir -p /etc/pihole/
2004
2005 stop_service dnsmasq
2006 if [[ "${INSTALL_WEB}" == true ]]; then
2007 stop_service lighttpd
2008 fi
2009 # Determine available interfaces
2010 get_available_interfaces
2011 # Find interfaces and let the user choose one
2012 chooseInterface
2013 # Decide what upstream DNS Servers to use
2014 setDNS
2015 # Let the user decide if they want to block ads over IPv4 and/or IPv6
2016 use4andor6
2017 # Let the user decide if they want the web interface to be installed automatically
2018 setAdminFlag
2019 # Let the user decide if they want query logging enabled...
2020 setLogging
2021 # Clone/Update the repos
2022 clone_or_update_repos
2023
2024 # Install packages used by the Pi-hole
2025 if [[ "${INSTALL_WEB}" == true ]]; then
2026 # Install the Web dependencies
2027 DEPS=("${PIHOLE_DEPS[@]}" "${PIHOLE_WEB_DEPS[@]}")
2028 # Otherwise,
2029 else
2030 # just install the Core dependencies
2031 DEPS=("${PIHOLE_DEPS[@]}")
2032 fi
2033
2034 install_dependent_packages DEPS[@]
2035
2036 # On some systems, lighttpd is not enabled on first install. We need to enable it here if the user
2037 # has chosen to install the web interface, else the `LIGHTTPD_ENABLED` check will fail
2038 if [[ "${INSTALL_WEB}" == true ]]; then
2039 enable_service lighttpd
2040 fi
2041
2042 if [[ -x "$(command -v systemctl)" ]]; then
2043 # Value will either be 1, if true, or 0
2044 LIGHTTPD_ENABLED=$(systemctl is-enabled lighttpd | grep -c 'enabled' || true)
2045 else
2046 # Value will either be 1, if true, or 0
2047 LIGHTTPD_ENABLED=$(service lighttpd status | awk '/Loaded:/ {print $0}' | grep -c 'enabled' || true)
2048 fi
2049
2050 # Install and log everything to a file
2051 installPihole | tee -a /proc/$$/fd/3
2052 else
2053 # Source ${setupVars} to use predefined user variables in the functions
2054 source ${setupVars}
2055
2056 # Clone/Update the repos
2057 clone_or_update_repos
2058
2059 # Install packages used by the Pi-hole
2060 if [[ "${INSTALL_WEB}" == true ]]; then
2061 # Install the Web dependencies
2062 DEPS=("${PIHOLE_DEPS[@]}" "${PIHOLE_WEB_DEPS[@]}")
2063 # Otherwise,
2064 else
2065 # just install the Core dependencies
2066 DEPS=("${PIHOLE_DEPS[@]}")
2067 fi
2068 install_dependent_packages DEPS[@]
2069
2070 if [[ -x "$(command -v systemctl)" ]]; then
2071 # Value will either be 1, if true, or 0
2072 LIGHTTPD_ENABLED=$(systemctl is-enabled lighttpd | grep -c 'enabled' || true)
2073 else
2074 # Value will either be 1, if true, or 0
2075 LIGHTTPD_ENABLED=$(service lighttpd status | awk '/Loaded:/ {print $0}' | grep -c 'enabled' || true)
2076 fi
2077 updatePihole | tee -a /proc/$$/fd/3
2078 fi
2079
2080 # Copy the temp log file into final log location for storage
2081 copy_to_install_log
2082
2083 if [[ "${INSTALL_WEB}" == true ]]; then
2084 # Add password to web UI if there is none
2085 pw=""
2086 # If no password is set,
2087 if [[ $(grep 'WEBPASSWORD' -c /etc/pihole/setupVars.conf) == 0 ]] ; then
2088 # generate a random password
2089 pw=$(tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c 8)
2090 # shellcheck disable=SC1091
2091 . /opt/pihole/webpage.sh
2092 echo "WEBPASSWORD=$(HashPassword ${pw})" >> ${setupVars}
2093 fi
2094 fi
2095
2096 echo -e " ${INFO} Restarting services..."
2097 # Start services
2098 start_service dnsmasq
2099 enable_service dnsmasq
2100
2101 # If the Web server was installed,
2102 if [[ "${INSTALL_WEB}" == true ]]; then
2103
2104 if [[ "${LIGHTTPD_ENABLED}" == "1" ]]; then
2105 start_service lighttpd
2106 enable_service lighttpd
2107 else
2108 echo -e " ${INFO} Lighttpd is disabled, skipping service restart"
2109 fi
2110 fi
2111
2112 # Enable FTL
2113 start_service pihole-FTL
2114 enable_service pihole-FTL
2115
2116 # Download and compile the aggregated block list
2117 runGravity
2118
2119 # Force an update of the updatechecker
2120 . /opt/pihole/updatecheck.sh
2121 . /opt/pihole/updatecheck.sh x remote
2122
2123 #
2124 if [[ "${useUpdateVars}" == false ]]; then
2125 displayFinalMessage "${pw}"
2126 fi
2127
2128 # If the Web interface was installed,
2129 if [[ "${INSTALL_WEB}" == true ]]; then
2130 # If there is a password,
2131 if (( ${#pw} > 0 )) ; then
2132 # display the password
2133 echo -e " ${INFO} Web Interface password: ${COL_LIGHT_GREEN}${pw}${COL_NC}
2134 This can be changed using 'pihole -a -p'\\n"
2135 fi
2136 fi
2137
2138 #
2139 if [[ "${useUpdateVars}" == false ]]; then
2140 # If the Web interface was installed,
2141 if [[ "${INSTALL_WEB}" == true ]]; then
2142 echo -e " View the web interface at http://pi.hole/admin or http://${IPV4_ADDRESS%/*}/admin"
2143 echo ""
2144 fi
2145 # Explain to the user how to use Pi-hole as their DNS server
2146 echo " You may now configure your devices to use the Pi-hole as their DNS server"
2147 [[ -n "${IPV4_ADDRESS%/*}" ]] && echo -e " ${INFO} Pi-hole DNS (IPv4): ${IPV4_ADDRESS%/*}"
2148 [[ -n "${IPV6_ADDRESS}" ]] && echo -e " ${INFO} Pi-hole DNS (IPv6): ${IPV6_ADDRESS}"
2149 echo -e " If you set a new IP address, please restart the server running the Pi-hole"
2150 #
2151 INSTALL_TYPE="Installation"
2152 else
2153 #
2154 INSTALL_TYPE="Update"
2155 fi
2156
2157 # Display where the log file is
2158 echo -e "\\n ${INFO} The install log is located at: ${installLogLoc}
2159 ${COL_LIGHT_GREEN}${INSTALL_TYPE} Complete! ${COL_NC}"
2160
2161}
2162
2163#
2164if [[ "${PH_TEST}" != true ]] ; then
2165 main "$@"
2166fi