#!/bin/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" readonly wildcardlist="/etc/dnsmasq.d/03-pihole-wildcard.conf" readonly colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" source "${colfile}" resolver="pihole-FTL" # Must be root to use this tool if [[ ! $EUID -eq 0 ]];then if [[ -x "$(command -v sudo)" ]]; then exec sudo bash "$0" "$@" exit $? else echo -e " ${CROSS} sudo is needed to run pihole commands. Please run this script as root or install sudo." exit 1 fi fi webpageFunc() { source "${PI_HOLE_SCRIPT_DIR}/webpage.sh" main "$@" exit 0 } whitelistFunc() { "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" exit 0 } blacklistFunc() { "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" exit 0 } wildcardFunc() { "${PI_HOLE_SCRIPT_DIR}"/list.sh "$@" exit 0 } debugFunc() { local automated local web # Pull off the `debug` leaving passed call augmentation flags in $1 shift if [[ "$@" == *"-a"* ]]; then automated="true" fi if [[ "$@" == *"-w"* ]]; then web="true" fi AUTOMATED=${automated:-} WEBCALL=${web:-} "${PI_HOLE_SCRIPT_DIR}"/piholeDebug.sh exit 0 } flushFunc() { "${PI_HOLE_SCRIPT_DIR}"/piholeLogFlush.sh "$@" exit 0 } updatePiholeFunc() { "${PI_HOLE_SCRIPT_DIR}"/update.sh exit 0 } reconfigurePiholeFunc() { /etc/.pihole/automated\ install/basic-install.sh --reconfigure exit 0; } updateGravityFunc() { "${PI_HOLE_SCRIPT_DIR}"/gravity.sh "$@" exit 0 } # Scan an array of files for matching strings scanList(){ # Escape full stops local domain="${1//./\\.}" lists="${2}" type="${3:-}" # Prevent grep from printing file path cd "/etc/pihole" || exit 1 # Prevent grep -i matching slowly: http://bit.ly/2xFXtUX export LC_CTYPE=C # /dev/null forces filename to be printed when only one list has been generated # shellcheck disable=SC2086 case "${type}" in "exact" ) grep -i -E -l "(^|\\s)${domain}($|\\s|#)" ${lists} /dev/null;; "wc" ) grep -i -o -m 1 "/${domain}/" ${lists};; * ) grep -i "${domain}" ${lists} /dev/null;; esac } # Print each subdomain # e.g: foo.bar.baz.com = "foo.bar.baz.com bar.baz.com baz.com com" processWildcards() { IFS="." read -r -a array <<< "${1}" for (( i=${#array[@]}-1; i>=0; i-- )); do ar="" for (( j=${#array[@]}-1; j>${#array[@]}-i-2; j-- )); do if [[ $j == $((${#array[@]}-1)) ]]; then ar="${array[$j]}" else ar="${array[$j]}.${ar}" fi done echo "${ar}" done } queryFunc() { shift local options="$*" adlist="" all="" exact="" blockpage="" matchType="match" if [[ "${options}" == "-h" ]] || [[ "${options}" == "--help" ]]; then echo "Usage: pihole -q [option] Example: 'pihole -q -exact domain.com' Query the adlists for a specified domain Options: -adlist Print the name of the block list URL -exact Search the block lists for exact domain matches -all Return all query matches within a block list -h, --help Show this help dialog" exit 0 fi if [[ ! -e "/etc/pihole/adlists.list" ]]; then echo -e "${COL_LIGHT_RED}The file '/etc/pihole/adlists.list' was not found${COL_NC}" exit 1 fi # Handle valid options if [[ "${options}" == *"-bp"* ]]; then exact="exact"; blockpage=true else [[ "${options}" == *"-adlist"* ]] && adlist=true [[ "${options}" == *"-all"* ]] && all=true if [[ "${options}" == *"-exact"* ]]; then exact="exact"; matchType="exact ${matchType}" fi fi # Strip valid options, leaving only the domain and invalid options # This allows users to place the options before or after the domain options=$(sed -E 's/ ?-(bp|adlists?|all|exact) ?//g' <<< "${options}") # Handle remaining options # If $options contain non ASCII characters, convert to punycode case "${options}" in "" ) str="No domain specified";; *" "* ) str="Unknown query option specified";; *[![:ascii:]]* ) domainQuery=$(idn2 "${options}");; * ) domainQuery="${options}";; esac if [[ -n "${str:-}" ]]; then echo -e "${str}${COL_NC}\\nTry 'pihole -q --help' for more information." exit 1 fi # Scan Whitelist and Blacklist lists="whitelist.txt blacklist.txt" mapfile -t results <<< "$(scanList "${domainQuery}" "${lists}" "${exact}")" if [[ -n "${results[*]}" ]]; then wbMatch=true # Loop through each result in order to print unique file title once for result in "${results[@]}"; do fileName="${result%%.*}" if [[ -n "${blockpage}" ]]; then echo "π ${result}" exit 0 elif [[ -n "${exact}" ]]; then echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}" else # Only print filename title once per file if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then echo " ${matchType^} found in ${COL_BOLD}${fileName^}${COL_NC}" fileName_prev="${fileName}" fi echo " ${result#*:}" fi done fi # Scan Wildcards if [[ -e "${wildcardlist}" ]]; then # Determine all subdomains, domain and TLDs mapfile -t wildcards <<< "$(processWildcards "${domainQuery}")" for match in "${wildcards[@]}"; do # Search wildcard list for matches mapfile -t results <<< "$(scanList "${match}" "${wildcardlist}" "wc")" if [[ -n "${results[*]}" ]]; then if [[ -z "${wcMatch:-}" ]] && [[ -z "${blockpage}" ]]; then wcMatch=true echo " ${matchType^} found in ${COL_BOLD}Wildcards${COL_NC}:" fi case "${blockpage}" in true ) echo "π ${wildcardlist##*/}"; exit 0;; * ) echo " *.${match}";; esac fi done fi # Get version sorted *.domains filenames (without dir path) lists=("$(cd "/etc/pihole" || exit 0; printf "%s\\n" -- *.domains | sort -V)") # Query blocklists for occurences of domain mapfile -t results <<< "$(scanList "${domainQuery}" "${lists[*]}" "${exact}")" # Handle notices if [[ -z "${wbMatch:-}" ]] && [[ -z "${wcMatch:-}" ]] && [[ -z "${results[*]}" ]]; then echo -e " ${INFO} No ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} found within block lists" exit 0 elif [[ -z "${results[*]}" ]]; then # Result found in WL/BL/Wildcards exit 0 elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 100 ]]; then echo -e " ${INFO} Over 100 ${exact/t/t }results found for ${COL_BOLD}${domainQuery}${COL_NC} This can be overridden using the -all option" exit 0 fi # Remove unwanted content from non-exact $results if [[ -z "${exact}" ]]; then # Delete lines starting with # # Remove comments after domain # Remove hosts format IP address mapfile -t results <<< "$(IFS=$'\n'; sed \ -e "/:#/d" \ -e "s/[ \\t]#.*//g" \ -e "s/:.*[ \\t]/:/g" \ <<< "${results[*]}")" # Exit if result was in a comment [[ -z "${results[*]}" ]] && exit 0 fi # Get adlist file content as array if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then for adlistUrl in $(< "/etc/pihole/adlists.list"); do if [[ "${adlistUrl:0:4}" =~ (http|www.) ]]; then adlists+=("${adlistUrl}") fi done fi # Print "Exact matches for" title if [[ -n "${exact}" ]] && [[ -z "${blockpage}" ]]; then plural=""; [[ "${#results[*]}" -gt 1 ]] && plural="es" echo " ${matchType^}${plural} for ${COL_BOLD}${domainQuery}${COL_NC} found in:" fi for result in "${results[@]}"; do fileName="${result/:*/}" # Determine *.domains URL using filename's number if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then fileNum="${fileName/list./}"; fileNum="${fileNum%%.*}" fileName="${adlists[$fileNum]}" # Discrepency occurs when adlists has been modified, but Gravity has not been run if [[ -z "${fileName}" ]]; then fileName="${COL_LIGHT_RED}(no associated adlists URL found)${COL_NC}" fi fi if [[ -n "${blockpage}" ]]; then echo "${fileNum} ${fileName}" elif [[ -n "${exact}" ]]; then echo " ${fileName}" else if [[ ! "${fileName}" == "${fileName_prev:-}" ]]; then count="" echo " ${matchType^} found in ${COL_BOLD}${fileName}${COL_NC}:" fileName_prev="${fileName}" fi : $((count++)) # Print matching domain if $max_count has not been reached [[ -z "${all}" ]] && max_count="50" if [[ -z "${all}" ]] && [[ "${count}" -ge "${max_count}" ]]; then [[ "${count}" -gt "${max_count}" ]] && continue echo " ${COL_GRAY}Over ${count} results found, skipping rest of file${COL_NC}" else echo " ${result#*:}" fi fi done exit 0 } chronometerFunc() { shift "${PI_HOLE_SCRIPT_DIR}"/chronometer.sh "$@" exit 0 } uninstallFunc() { "${PI_HOLE_SCRIPT_DIR}"/uninstall.sh exit 0 } versionFunc() { shift "${PI_HOLE_SCRIPT_DIR}"/version.sh "$@" exit 0 } restartDNS() { local svcOption svc str output status svcOption="${1:-}" # Determine if we should reload or restart restart if [[ "${svcOption}" =~ "reload" ]]; then # Using SIGHUP will NOT re-read any *.conf files svc="killall -s SIGHUP ${resolver}" else # Get PID of resolver to determine if it needs to start or restart if pidof pihole-FTL &> /dev/null; then svcOption="restart" else svcOption="start" fi svc="service ${resolver} ${svcOption}" fi # Print output to Terminal, but not to Web Admin str="${svcOption^}ing DNS service" [[ -t 1 ]] && echo -ne " ${INFO} ${str}..." output=$( { ${svc}; } 2>&1 ) status="$?" if [[ "${status}" -eq 0 ]]; then [[ -t 1 ]] && echo -e "${OVER} ${TICK} ${str}" else [[ ! -t 1 ]] && local OVER="" echo -e "${OVER} ${CROSS} ${output}" fi } piholeEnable() { if [[ "${2}" == "-h" ]] || [[ "${2}" == "--help" ]]; then echo "Usage: pihole disable [time] Example: 'pihole disable', or 'pihole disable 5m' Disable Pi-hole subsystems Time: #s Disable Pi-hole functionality for # second(s) #m Disable Pi-hole functionality for # minute(s)" exit 0 elif [[ "${1}" == "0" ]]; then # Disable Pi-hole sed -i 's/^addn-hosts=\/etc\/pihole\/gravity.list/#addn-hosts=\/etc\/pihole\/gravity.list/' /etc/dnsmasq.d/01-pihole.conf sed -i 's/^addn-hosts=\/etc\/pihole\/black.list/#addn-hosts=\/etc\/pihole\/black.list/' /etc/dnsmasq.d/01-pihole.conf if [[ -e "$wildcardlist" ]]; then mv "$wildcardlist" "/etc/pihole/wildcard.list" fi if [[ $# > 1 ]]; then local error=false if [[ "${2}" == *"s" ]]; then tt=${2%"s"} if [[ "${tt}" =~ ^-?[0-9]+$ ]];then local str="Disabling blocking for ${tt} seconds" echo -e " ${INFO} ${str}..." local str="Blocking will be re-enabled in ${tt} seconds" nohup bash -c "sleep ${tt}; pihole enable" /dev/null & else local error=true fi elif [[ "${2}" == *"m" ]]; then tt=${2%"m"} if [[ "${tt}" =~ ^-?[0-9]+$ ]];then local str="Disabling blocking for ${tt} minutes" echo -e " ${INFO} ${str}..." local str="Blocking will be re-enabled in ${tt} minutes" tt=$((${tt}*60)) nohup bash -c "sleep ${tt}; pihole enable" /dev/null & else local error=true fi elif [[ -n "${2}" ]]; then local error=true else echo -e " ${INFO} Disabling blocking" fi if [[ ${error} == true ]];then echo -e " ${COL_LIGHT_RED}Unknown format for delayed reactivation of the blocking!${COL_NC}" echo -e " Try 'pihole disable --help' for more information." exit 1 fi local str="Pi-hole Disabled" fi else # Enable Pi-hole echo -e " ${INFO} Enabling blocking" local str="Pi-hole Enabled" sed -i 's/^#addn-hosts/addn-hosts/' /etc/dnsmasq.d/01-pihole.conf if [[ -e "/etc/pihole/wildcard.list" ]]; then mv "/etc/pihole/wildcard.list" "$wildcardlist" fi fi restartDNS 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.log off Disable and flush the Pi-hole log at /var/log/pihole.log off noflush Disable the Pi-hole log at /var/log/pihole.log" exit 0 elif [[ "${1}" == "off" ]]; then # Disable logging sed -i 's/^log-queries/#log-queries/' /etc/dnsmasq.d/01-pihole.conf sed -i 's/^QUERY_LOGGING=true/QUERY_LOGGING=false/' /etc/pihole/setupVars.conf if [[ "${2}" != "noflush" ]]; then # Flush logs pihole -f fi echo -e " ${INFO} Disabling logging..." local str="Logging has been disabled!" elif [[ "${1}" == "on" ]]; then # Enable logging sed -i 's/^#log-queries/log-queries/' /etc/dnsmasq.d/01-pihole.conf sed -i 's/^QUERY_LOGGING=false/QUERY_LOGGING=true/' /etc/pihole/setupVars.conf 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}" } statusFunc() { local addnConfigs # Determine if service is running on port 53 (Cr: https://superuser.com/a/806331) if (echo > /dev/tcp/127.0.0.1/53) >/dev/null 2>&1; then if [[ "${1}" != "web" ]]; then echo -e " ${TICK} DNS service is running" fi else case "${1}" in "web") echo "-1";; *) echo -e " ${CROSS} DNS service is NOT running";; esac return 0 fi # Determine if Pi-hole's addn-hosts configs are commented out addnConfigs=$(grep -i "addn-hosts=/" /etc/dnsmasq.d/01-pihole.conf) if [[ "${addnConfigs}" =~ "#" ]]; then # A config is commented out case "${1}" in "web") echo 0;; *) echo -e " ${CROSS} Pi-hole blocking is Disabled";; esac elif [[ -n "${addnConfigs}" ]]; then # Configs are set case "${1}" in "web") echo 1;; *) echo -e " ${TICK} Pi-hole blocking is Enabled";; esac else # No configs were found case "${1}" in "web") echo 99;; *) echo -e " ${INFO} No hosts file linked to dnsmasq, adding it in enabled state";; esac # Add addn-host= to dnsmasq echo "addn-hosts=/etc/pihole/gravity.list" >> /etc/dnsmasq.d/01-pihole.conf restartDNS fi } tailFunc() { echo -e " ${INFO} Press Ctrl-C to exit" # Retrieve IPv4/6 addresses source /etc/pihole/setupVars.conf # Strip date from each line # Colour blocklist/blacklist/wildcard entries as red # Colour A/AAAA/DHCP strings as white # Colour everything else as gray tail -f /var/log/pihole.log | sed -E \ -e "s,($(date +'%b %d ')| dnsmasq[.*[0-9]]),,g" \ -e "s,(.*(gravity.list|black.list| config ).* is (${IPV4_ADDRESS%/*}|${IPV6_ADDRESS:-NULL}).*),${COL_RED}&${COL_NC}," \ -e "s,.*(query\\[A|DHCP).*,${COL_NC}&${COL_NC}," \ -e "s,.*,${COL_GRAY}&${COL_NC}," exit 0 } piholeCheckoutFunc() { 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 Admin Console 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" exit 0 fi source "${PI_HOLE_SCRIPT_DIR}"/piholeCheckout.sh shift checkout "$@" } tricorderFunc() { if [[ ! -p "/dev/stdin" ]]; then echo -e " ${INFO} Please do not call Tricorder directly" exit 1 fi if ! (echo > /dev/tcp/tricorder.pi-hole.net/9998) >/dev/null 2>&1; then echo -e " ${CROSS} Unable to connect to Pi-hole's Tricorder server" exit 1 fi if command -v openssl &> /dev/null; then openssl s_client -quiet -connect tricorder.pi-hole.net:9998 2> /dev/null < /dev/stdin exit "$?" else echo -e " ${INFO} ${COL_YELLOW}Security Notice${COL_NC}: ${COL_WHITE}openssl${COL_NC} is not installed Your debug log will be transmitted unencrypted via plain-text There is a possibility that this could be intercepted by a third party If you wish to cancel, press Ctrl-C to exit within 10 seconds" secs="10" while [[ "$secs" -gt "0" ]]; do echo -ne "." sleep 1 : $((secs--)) done echo " " nc tricorder.pi-hole.net 9999 < /dev/stdin exit "$?" fi } updateCheckFunc() { "${PI_HOLE_SCRIPT_DIR}"/updatecheck.sh "$@" exit 0 } helpFunc() { echo "Usage: pihole [options] Example: 'pihole -w -h' Add '-h' after specific commands for more information on usage Whitelist/Blacklist Options: -w, whitelist Whitelist domain(s) -b, blacklist Blacklist domain(s) -wild, wildcard Blacklist domain(s), and all its subdomains Add '-h' for more info on whitelist/blacklist usage Debugging Options: -d, debug Start a debugging session Add '-a' to enable automated debugging -f, flush Flush the Pi-hole log -r, reconfigure Reconfigure or Repair Pi-hole subsystems -t, tail View the live output of the Pi-hole log Options: -a, admin Admin Console options Add '-h' for more info on admin console usage -c, chronometer Calculates stats and displays to an LCD Add '-h' for more info on chronometer usage -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 -v, version Show installed versions of Pi-hole, Admin Console & FTL Add '-h' for more info on version usage 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 Restart Pi-hole subsystems checkout Switch Pi-hole subsystems to a different Github branch Add '-h' for more info on checkout usage"; exit 0 } if [[ $# = 0 ]]; then helpFunc fi # Handle redirecting to specific functions based on arguments case "${1}" in "-w" | "whitelist" ) whitelistFunc "$@";; "-b" | "blacklist" ) blacklistFunc "$@";; "-wild" | "wildcard" ) wildcardFunc "$@";; "-d" | "debug" ) debugFunc "$@";; "-f" | "flush" ) flushFunc "$@";; "-up" | "updatePihole" ) updatePiholeFunc;; "-r" | "reconfigure" ) reconfigurePiholeFunc;; "-g" | "updateGravity" ) updateGravityFunc "$@";; "-c" | "chronometer" ) chronometerFunc "$@";; "-h" | "help" ) helpFunc;; "-v" | "version" ) versionFunc "$@";; "-q" | "query" ) queryFunc "$@";; "-l" | "logging" ) piholeLogging "$@";; "uninstall" ) uninstallFunc;; "enable" ) piholeEnable 1;; "disable" ) piholeEnable 0 "$2";; "status" ) statusFunc "$2";; "restartdns" ) restartDNS "$2";; "-a" | "admin" ) webpageFunc "$@";; "-t" | "tail" ) tailFunc;; "checkout" ) piholeCheckoutFunc "$@";; "tricorder" ) tricorderFunc;; "updatechecker" ) updateCheckFunc "$@";; * ) helpFunc;; esac