#!/usr/bin/env bash # Pi-hole: A black hole for Internet advertisements # (c) 2017 Pi-hole, LLC (https://pi-hole.net) # Network-wide ad blocking via your own hardware. # # Controller for all pihole scripts and functions. # # This file is copyright under the latest version of the EUPL. # Please see LICENSE file for your rights under this license. readonly PI_HOLE_SCRIPT_DIR="/opt/pihole" # PI_HOLE_BIN_DIR is not readonly here because in some functions (checkout), # they might get set again when the installer is sourced. This causes an # error due to modifying a readonly variable. PI_HOLE_BIN_DIR="/usr/local/bin" readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" source "${colfile}" readonly utilsfile="${PI_HOLE_SCRIPT_DIR}/utils.sh" source "${utilsfile}" # Source api functions readonly apifile="${PI_HOLE_SCRIPT_DIR}/api.sh" source "${apifile}" versionsfile="/etc/pihole/versions" if [ -f "${versionsfile}" ]; then # Only source versionsfile if the file exits # fixes a warning during installation where versionsfile does not exist yet # but gravity calls `pihole -status` and thereby sourcing the file source "${versionsfile}" fi # TODO: We can probably remove the reliance on this function too, just tell people to pihole-FTL --config webserver.api.password "password" SetWebPassword() { if [ -n "$2" ] ; then readonly PASSWORD="$2" readonly CONFIRM="${PASSWORD}" else # Prevents a bug if the user presses Ctrl+C and it continues to hide the text typed. # So we reset the terminal via stty if the user does press Ctrl+C trap '{ echo -e "\nNot changed" ; stty sane ; exit 1; }' INT read -s -r -p "Enter New Password (Blank for no password): " PASSWORD echo "" if [ "${PASSWORD}" == "" ]; then setFTLConfigValue "webserver.api.password" "" echo -e " ${TICK} Password Removed" exit 0 fi read -s -r -p "Confirm Password: " CONFIRM echo "" fi if [ "${PASSWORD}" == "${CONFIRM}" ] ; then # pihole-FTL will automatically hash the password setFTLConfigValue "webserver.api.password" "${PASSWORD}" echo -e " ${TICK} New password set" else echo -e " ${CROSS} Passwords don't match. Your password has not been changed" exit 1 fi } listFunc() { "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" exit 0 } debugFunc() { local automated local web local check_database_integrity # Pull off the `debug` leaving passed call augmentation flags in $1 shift for value in "$@"; do [[ "$value" == *"-a"* ]] && automated="true" [[ "$value" == *"-w"* ]] && web="true" [[ "$value" == *"-c"* ]] && check_database_integrity="true" [[ "$value" == *"--check_database"* ]] && check_database_integrity="true" done AUTOMATED=${automated:-} WEBCALL=${web:-} CHECK_DATABASE=${check_database_integrity:-} "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh exit 0 } flushFunc() { "${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@" exit 0 } arpFunc() { "${PI_HOLE_SCRIPT_DIR}"/piholeARPTable.sh "$@" exit 0 } updatePiholeFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else shift "${PI_HOLE_SCRIPT_DIR}"/update.sh "$@" exit 0 fi } reconfigurePiholeFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else /etc/.pihole/automated\ install/basic-install.sh --reconfigure exit 0; fi } updateGravityFunc() { exec "${PI_HOLE_SCRIPT_DIR}"/gravity.sh "$@" } queryFunc() { shift "${PI_HOLE_SCRIPT_DIR}"/query.sh "$@" exit 0 } chronometerFunc() { echo "Chronometer is gone, use PADD (https://github.com/pi-hole/PADD)" exit 0 } uninstallFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else "${PI_HOLE_SCRIPT_DIR}"/uninstall.sh exit 0 fi } versionFunc() { exec "${PI_HOLE_SCRIPT_DIR}"/version.sh } restartDNS() { local svcOption svc str output status pid icon FTL_PID_FILE svcOption="${1:-restart}" # get the current path to the pihole-FTL.pid FTL_PID_FILE="$(getFTLConfigValue files.pid)" # Determine if we should reload or restart if [[ "${svcOption}" =~ "reload-lists" ]]; then # Reloading of the lists has been requested # Note 1: This will NOT re-read any *.conf files # Note 2: We cannot use killall here as it does # not know about real-time signals pid="$(getFTLPID ${FTL_PID_FILE})" if [[ "$pid" -eq "-1" ]]; then svc="true" str="FTL is not running" icon="${INFO}" else svc="kill -RTMIN ${pid}" str="Reloading DNS lists" icon="${TICK}" fi elif [[ "${svcOption}" =~ "reload" ]]; then # Reloading of the DNS cache has been requested # Note: This will NOT re-read any *.conf files pid="$(getFTLPID ${FTL_PID_FILE})" if [[ "$pid" -eq "-1" ]]; then svc="true" str="FTL is not running" icon="${INFO}" else svc="kill -HUP ${pid}" str="Flushing DNS cache" icon="${TICK}" fi else # A full restart has been requested svc="service pihole-FTL restart" str="Restarting DNS server" icon="${TICK}" fi # Print output to Terminal, but not to Web Admin [[ -t 1 ]] && echo -ne " ${INFO} ${str}..." output=$( { ${svc}; } 2>&1 ) status="$?" if [[ "${status}" -eq 0 ]]; then [[ -t 1 ]] && echo -e "${OVER} ${icon} ${str}" return 0 else [[ ! -t 1 ]] && local OVER="" echo -e "${OVER} ${CROSS} ${output}" return 1 fi } piholeEnable() { if [[ "${2}" == "-h" ]] || [[ "${2}" == "--help" ]]; then echo "Usage: pihole enable/disable [time] Example: 'pihole enable', or 'pihole disable 5m' En- or disable Pi-hole subsystems Time: #s En-/disable Pi-hole functionality for # second(s) #m En-/disable Pi-hole functionality for # minute(s)" exit 0 fi # Get timer local tt="null" if [[ $# -gt 1 ]]; then local error=false if [[ "${2}" == *"s" ]]; then tt=${2%"s"} if [[ ! "${tt}" =~ ^-?[0-9]+$ ]];then local error=true fi elif [[ "${2}" == *"m" ]]; then tt=${2%"m"} if [[ "${tt}" =~ ^-?[0-9]+$ ]];then tt=$((${tt}*60)) else local error=true fi elif [[ -n "${2}" ]]; then local error=true fi if [[ ${error} == true ]];then echo -e " ${COL_LIGHT_RED}Unknown format for blocking timer!${COL_NC}" echo -e " Try 'pihole disable --help' for more information." exit 1 fi fi # Authenticate with the API LoginAPI # Send the request data=$(PostFTLData "dns/blocking" "{ \"blocking\": ${1}, \"timer\": ${tt} }") # Check the response local extra=" forever" local timer="$(echo "${data}"| jq --raw-output '.timer' )" if [[ "${timer}" != "null" ]]; then extra=" for ${timer}s" fi local str="Pi-hole $(echo "${data}" | jq --raw-output '.blocking')${extra}" # Logout from the API LogoutAPI echo -e "${OVER} ${TICK} ${str}" } piholeLogging() { shift if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]]; then echo "Usage: pihole logging [options] Example: 'pihole logging on' Specify whether the Pi-hole log should be used Options: on Enable the Pi-hole log at /var/log/pihole/pihole.log off Disable and flush the Pi-hole log at /var/log/pihole/pihole.log off noflush Disable the Pi-hole log at /var/log/pihole/pihole.log" exit 0 elif [[ "${1}" == "off" ]]; then # Disable logging setFTLConfigValue dns.queryLogging false if [[ "${2}" != "noflush" ]]; then # Flush logs "${PI_HOLE_BIN_DIR}"/pihole -f fi echo -e " ${INFO} Disabling logging..." local str="Logging has been disabled!" elif [[ "${1}" == "on" ]]; then # Enable logging setFTLConfigValue dns.queryLogging true echo -e " ${INFO} Enabling logging..." local str="Logging has been enabled!" else echo -e " ${COL_LIGHT_RED}Invalid option${COL_NC} Try 'pihole logging --help' for more information." exit 1 fi restartDNS echo -e "${OVER} ${TICK} ${str}" } analyze_ports() { local lv4 lv6 port=${1} # FTL is listening at least on at least one port when this # function is getting called # Check individual address family/protocol combinations # For a healthy Pi-hole, they should all be up (nothing printed) lv4="$(ss --ipv4 --listening --numeric --tcp --udp src :${port})" if grep -q "udp " <<< "${lv4}"; then echo -e " ${TICK} UDP (IPv4)" else echo -e " ${CROSS} UDP (IPv4)" fi if grep -q "tcp " <<< "${lv4}"; then echo -e " ${TICK} TCP (IPv4)" else echo -e " ${CROSS} TCP (IPv4)" fi lv6="$(ss --ipv6 --listening --numeric --tcp --udp src :${port})" if grep -q "udp " <<< "${lv6}"; then echo -e " ${TICK} UDP (IPv6)" else echo -e " ${CROSS} UDP (IPv6)" fi if grep -q "tcp " <<< "${lv6}"; then echo -e " ${TICK} TCP (IPv6)" else echo -e " ${CROSS} TCP (IPv6)" fi echo "" } statusFunc() { # Determine if there is pihole-FTL service is listening local pid port ftl_pid_file block_status ftl_pid_file="$(getFTLConfigValue files.pid)" pid="$(getFTLPID ${ftl_pid_file})" if [[ "$pid" -eq "-1" ]]; then case "${1}" in "web") echo "-1";; *) echo -e " ${CROSS} DNS service is NOT running";; esac return 0 else # get the DNS port pihole-FTL is listening on port="$(getFTLConfigValue dns.port)" if [[ "${port}" == "0" ]]; then case "${1}" in "web") echo "-1";; *) echo -e " ${CROSS} DNS service is NOT listening";; esac return 0 else if [[ "${1}" != "web" ]]; then echo -e " ${TICK} FTL is listening on port ${port}" analyze_ports "${port}" fi fi fi # Determine if Pi-hole's blocking is enabled block_status=$(getFTLConfigValue dns.blocking.active) if [ ${block_status} == "true" ]; then case "${1}" in "web") echo "$port";; *) echo -e " ${TICK} Pi-hole blocking is enabled";; esac else case "${1}" in "web") echo 0;; *) echo -e " ${CROSS} Pi-hole blocking is disabled";; esac fi exit 0 } tailFunc() { # Warn user if Pi-hole's logging is disabled local logging_enabled=$(getFTLConfigValue dns.queryLogging) if [[ "${logging_enabled}" != "true" ]]; then echo " ${CROSS} Warning: Query logging is disabled" fi echo -e " ${INFO} Press Ctrl-C to exit" # Get logfile path readonly LOGFILE=$(getFTLConfigValue files.log.dnsmasq) # Strip date from each line # Color blocklist/denylist/wildcard entries as red # Color A/AAAA/DHCP strings as white # Color everything else as gray tail -f $LOGFILE | grep --line-buffered "${1}" | sed -E \ -e "s,($(date +'%b %d ')| dnsmasq\[[0-9]*\]),,g" \ -e "s,(.*(denied |gravity blocked ).*),${COL_RED}&${COL_NC}," \ -e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \ -e "s,.*,${COL_GRAY}&${COL_NC}," exit 0 } piholeCheckoutFunc() { if [ -n "${DOCKER_VERSION}" ]; then unsupportedFunc else if [[ "$2" == "-h" ]] || [[ "$2" == "--help" ]]; then echo "Usage: pihole checkout [repo] [branch] Example: 'pihole checkout master' or 'pihole checkout core dev' Switch Pi-hole subsystems to a different GitHub branch Repositories: core [branch] Change the branch of Pi-hole's core subsystem web [branch] Change the branch of Web Interface subsystem ftl [branch] Change the branch of Pi-hole's FTL subsystem Branches: master Update subsystems to the latest stable release dev Update subsystems to the latest development release branchname Update subsystems to the specified branchname" exit 0 fi source "${PI_HOLE_SCRIPT_DIR}"/piholeCheckout.sh shift checkout "$@" fi } tricorderFunc() { local tricorder_token if [[ ! -p "/dev/stdin" ]]; then echo -e " ${INFO} Please do not call Tricorder directly" exit 1 fi tricorder_token=$(curl --silent --fail --show-error --upload-file "-" https://tricorder.pi-hole.net/upload < /dev/stdin 2>&1) if [[ "${tricorder_token}" != "https://tricorder.pi-hole.net/"* ]]; then echo -e "${CROSS} uploading failed, contact Pi-hole support for assistance." # Log curl error (if available) if [ -n "${tricorder_token}" ]; then echo -e "${INFO} Error message: ${COL_RED}${tricorder_token}${COL_NC}\\n" tricorder_token="" fi exit 1 fi echo "Upload successful, your token is: ${COL_GREEN}${tricorder_token}${COL_NC}" exit 0 } updateCheckFunc() { "${PI_HOLE_SCRIPT_DIR}"/updatecheck.sh "$@" exit 0 } unsupportedFunc(){ echo "Function not supported in Docker images" exit 0 } helpFunc() { echo "Usage: pihole [options] Example: 'pihole allow -h' Add '-h' after specific commands for more information on usage Domain Options: allow, allowlist Allow domain(s) deny, denylist Deny domain(s) --regex, regex Regex deny domains(s) --allow-regex Regex allow domains(s) --wild, wildcard Wildcard deny domain(s) --allow-wild Wildcard allow domain(s) Add '-h' for more info on allow/deny usage Debugging Options: -d, debug Start a debugging session Add '-c' or '--check-database' to include a Pi-hole database integrity check Add '-a' to automatically upload the log to tricorder.pi-hole.net -f, flush Flush the Pi-hole log -r, reconfigure Reconfigure or Repair Pi-hole subsystems -t, tail [arg] View the live output of the Pi-hole log. Add an optional argument to filter the log (regular expressions are supported) Options: setpassword [pwd] Set the password for the web interface Without optional argument, password is read interactively. When specifying a password directly, enclose it in single quotes. -g, updateGravity Update the list of ad-serving domains -h, --help, help Show this help dialog -l, logging Specify whether the Pi-hole log should be used Add '-h' for more info on logging usage -q, query Query the adlists for a specified domain Add '-h' for more info on query usage -up, updatePihole Update Pi-hole subsystems Add '--check-only' to exit script before update is performed. -v, version Show installed versions of Pi-hole, Web Interface & FTL uninstall Uninstall Pi-hole from your system status Display the running status of Pi-hole subsystems enable Enable Pi-hole subsystems disable Disable Pi-hole subsystems Add '-h' for more info on disable usage restartdns Full restart Pi-hole subsystems Add 'reload' to update the lists and flush the cache without restarting the DNS server Add 'reload-lists' to only update the lists WITHOUT flushing the cache or restarting the DNS server checkout Switch Pi-hole subsystems to a different GitHub branch Add '-h' for more info on checkout usage arpflush Flush information stored in Pi-hole's network tables"; exit 0 } if [[ $# = 0 ]]; then helpFunc fi # functions that do not require sudo power need_root=1 case "${1}" in "-h" | "help" | "--help" ) helpFunc;; "-v" | "version" ) versionFunc;; "-c" | "chronometer" ) chronometerFunc "$@";; "-q" | "query" ) queryFunc "$@";; "status" ) statusFunc "$2";; "tricorder" ) tricorderFunc;; # we need to add all arguments that require sudo power to not trigger the * argument "allow" | "allowlist" ) need_root=0;; "deny" | "denylist" ) need_root=0;; "--wild" | "wildcard" ) need_root=0;; "--regex" | "regex" ) need_root=0;; "--allow-regex" | "allow-regex" ) need_root=0;; "--allow-wild" | "allow-wild" ) need_root=0;; "-f" | "flush" ) ;; "-up" | "updatePihole" ) ;; "-r" | "reconfigure" ) ;; "-l" | "logging" ) ;; "uninstall" ) ;; "enable" ) need_root=0;; "disable" ) need_root=0;; "-d" | "debug" ) ;; "restartdns" ) ;; "-g" | "updateGravity" ) ;; "reloaddns" ) ;; "setpassword" ) ;; "checkout" ) ;; "updatechecker" ) ;; "arpflush" ) ;; "-t" | "tail" ) ;; * ) helpFunc;; esac # In the case of alpine running in a container, the USER variable appears to be blank # which prevents the next trap from working correctly. Set it by running whoami if [[ -z ${USER} ]]; then USER=$(whoami) fi # Check if the current user is neither root nor pihole and if the command # requires root. If so, exit with an error message. if [[ $EUID -ne 0 && ${USER} != "pihole" && need_root -eq 1 ]];then echo -e " ${CROSS} The Pi-hole command requires root privileges, try:" echo -e " ${COL_GREEN}sudo pihole $*${COL_NC}" exit 1 fi # Handle redirecting to specific functions based on arguments case "${1}" in "allow" | "allowlist" ) listFunc "$@";; "deny" | "denylist" ) listFunc "$@";; "--wild" | "wildcard" ) listFunc "$@";; "--regex" | "regex" ) listFunc "$@";; "--allow-regex" | "allow-regex" ) listFunc "$@";; "--allow-wild" | "allow-wild" ) listFunc "$@";; "-d" | "debug" ) debugFunc "$@";; "-f" | "flush" ) flushFunc "$@";; "-up" | "updatePihole" ) updatePiholeFunc "$@";; "-r" | "reconfigure" ) reconfigurePiholeFunc;; "-g" | "updateGravity" ) updateGravityFunc "$@";; "-l" | "logging" ) piholeLogging "$@";; "uninstall" ) uninstallFunc;; "enable" ) piholeEnable true "$2";; "disable" ) piholeEnable false "$2";; "restartdns" ) restartDNS "$2";; "reloaddns" ) restartDNS "reload";; "setpassword" ) SetWebPassword "$@";; "checkout" ) piholeCheckoutFunc "$@";; "updatechecker" ) shift; updateCheckFunc "$@";; "arpflush" ) arpFunc "$@";; "-t" | "tail" ) tailFunc "$2";; * ) helpFunc;; esac