#!/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" colfile="${PI_HOLE_SCRIPT_DIR}/COL_TABLE" source "${colfile}" # 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 } scanList(){ domain="${1}" list="${2}" method="${3}" # Switch folder, preventing grep from printing file path cd "/etc/pihole" || return 1 if [[ -n "${method}" ]]; then grep -i -E -l "(^|\s|\/)${domain}($|\s|\/)" ${list} /dev/null 2> /dev/null else grep -i "${domain}" ${list} /dev/null 2> /dev/null fi } 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() { options="$*" options="${options/-q /}" 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 [[ "${options}" == *"-exact"* ]]; then method="exact" exact=true fi if [[ "${options}" == *"-adlist"* ]]; then adlist=true fi if [[ "${options}" == *"-bp"* ]]; then method="exact" blockpage=true fi if [[ "${options}" == *"-all"* ]]; then all=true fi # Strip valid options, leaving only the domain and invalid options options=$(sed 's/ \?-\(exact\|adlist\|bp\|all\) \?//g' <<< "$options") # Handle errors if [[ "${options}" == *" "* ]]; then error=true str="Unknown option specified" elif [[ "${options}" == "-q" ]]; then error=true str="No domain specified" fi if [[ -n "${error}" ]]; then echo -e " ${COL_LIGHT_RED}${str}${COL_NC} Try 'pihole -q --help' for more information." exit 1 fi # If domain contains non ASCII characters, convert domain to punycode if python is available # Cr: https://serverfault.com/a/335079 if [[ "$options" = *[![:ascii:]]* ]]; then if command -v python &> /dev/null; then query=$(python -c 'import sys;print sys.argv[1].decode("utf-8").encode("idna")' "${options}") fi else query="${options}" fi # Scan Whitelist and Blacklist lists="whitelist.txt blacklist.txt" results=($(scanList "${query}" "${lists}" "${method}")) if [[ -n "${results[*]}" ]]; then # Loop through each scanList line to print appropriate title for result in "${results[@]}"; do filename="${result/:*/}" if [[ -n "$exact" ]]; then printf " Exact result in %s\n" "${filename}" elif [[ -n "$blockpage" ]]; then printf " [i] %s\n" "${filename}" else domain="${result/*:/}" if [[ ! "${filename}" == "${filename_prev:-}" ]]; then printf " Result from %s\n" "${filename}" fi printf " %s\n" "${domain}" filename_prev="${filename}" fi done fi # Scan Wildcards if [[ -e "${wildcardlist}" ]]; then wildcards=($(processWildcards "${query}")) for match in "${wildcards[@]}"; do results=($(scanList "\/${match}\/" ${wildcardlist})) if [[ -n "${results[*]}" ]]; then # Remove empty lines before couting number of results count=$(sed '/^\s*$/d' <<< "${results[@]}" | wc -l) if [[ "${count}" -ge 0 ]]; then blResult=true if [[ -z "${blockpage}" ]]; then printf " Wildcard result in %s\n" "${wildcardlist/*dnsmasq.d\/}" fi if [[ -n "${blockpage}" ]]; then echo " ${INFO} ${match}" else echo " *.${match}" fi fi fi done [[ -n "${blResult}" ]] && [[ -n "${blockpage}" ]] && exit 0 fi # Glob *.domains file names, remove file paths and sort by list number lists_raw=(/etc/pihole/*.domains) IFS_OLD=$IFS IFS=$'\n' lists=$(sort -t . -k 2 -g <<< "${lists_raw[*]//\/etc\/pihole\//}") # Scan Domains files results=($(scanList "${query}" "${lists}" "${method}")) # Handle notices if [[ -z "${blResult}" ]] && [[ -z "${results[*]}" ]]; then notice=true str="No ${method/t/t }results found for ${query} found within block lists" elif [[ -z "${all}" ]] && [[ "${#results[*]}" -ge 16000 ]]; then # 16000 chars is 15 chars X 1000 lines worth of results notice=true str="Hundreds of ${method/t/t }results found for ${query} This can be overriden using the -all option" fi if [[ -n "${notice}" ]]; then echo -e " ${INFO} ${str}" exit fi # Remove unwanted content from results if [[ -z "${method}" ]]; then results=($(sed "/:#/d" <<< "${results[*]}")) # Lines starting with comments results=($(sed "s/[ \t]#.*//g" <<< "${results[*]}")) # Comments after domain results=($(sed "s/:.*[ \t]/:/g" <<< "${results[*]}")) # IP address fi IFS=$IFS_OLD # Get adlist content as array if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then if [[ -f "/etc/pihole/adlists.list" ]]; then for url in $(< /etc/pihole/adlists.list); do if [[ "${url:0:4}" == "http" ]] || [[ "${url:0:3}" == "www" ]]; then adlists+=("$url") fi done else echo -e " ${COL_LIGHT_RED}The file '/etc/pihole/adlists.list' was not found${COL_NC}" exit 1 fi fi if [[ -n "${results[*]}" ]]; then if [[ -n "${exact}" ]]; then echo " Exact result(s) for ${query} found in:" fi for result in "${results[@]}"; do filename="${result/:*/}" # Convert file name to URL name for -adlist or -bp options if [[ -n "${adlist}" ]] || [[ -n "${blockpage}" ]]; then filenum=("${filename/list./}") filenum=("${filenum/.*/}") filename="${adlists[$filenum]}" fi if [[ -n "${exact}" ]]; then printf " %s\n" "${filename}" elif [[ -n "${blockpage}" ]]; then printf " [%s] %s\n" "${filenum}" "${filename}" else # Standard query output # Print filename heading once per file, not for every match if [[ ! "${filename}" == "${filename_prev:-}" ]]; then unset count printf " Result from %s\n" "${filename}" else let count++ fi # Print matching domain if $max_count has not been reached [[ -z "${all}" ]] && max_count="20" if [[ -z "${all}" ]] && [[ "${count}" -eq "${max_count}" ]]; then echo " Over $count results found, skipping rest of file" elif [[ -z "${all}" ]] && [[ "${count}" -gt "${max_count}" ]]; then continue else domain="${result/*:/}" printf " %s\n" "${domain}" fi filename_prev="${filename}" fi done fi 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 str output status over # Force-reload is used for "/etc/pihole/*.list" files # Restart is needed for "/etc/dnsmasq.d/*.conf" files svcOption="${1:-}" if [[ -z "${svcOption}" ]]; then # Get PID of dnsmasq to determine if it needs to start or restart if pidof dnsmasq &> /dev/null; then svcOption="restart" else svcOption="start" fi fi # Only print output to Terminal, not Web Admin str="${svcOption^}ing DNS service" [[ -t 1 ]] && echo -ne " ${INFO} ${str}..." output=$( { service dnsmasq "${svcOption}"; } 2>&1 ) status="$?" if [[ "$?" -eq 0 ]]; then [[ -t 1 ]] && echo -e "${OVER} ${TICK} ${str}" else [[ ! -t 1 ]] && 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 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 pihole -f 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}" } piholeStatus() { if [[ "$(netstat -plnt | grep -c ':53 ')" -gt "0" ]]; then if [[ "${1}" != "web" ]]; then echo -e " ${TICK} DNS service is running" fi else if [[ "${1}" == "web" ]]; then echo "-1"; else echo -e " ${CROSS} DNS service is NOT running" fi return fi if [[ "$(grep -i "^#addn-hosts=/" /etc/dnsmasq.d/01-pihole.conf)" ]]; then # List is commented out if [[ "${1}" == "web" ]]; then echo 0; else echo -e " ${CROSS} Pi-hole blocking is Disabled"; fi elif [[ "$(grep -i "^addn-hosts=/" /etc/dnsmasq.d/01-pihole.conf)" ]]; then # List set if [[ "${1}" == "web" ]]; then echo 1; else echo -e " ${TICK} Pi-hole blocking is Enabled"; fi else # Addn-host not found if [[ "${1}" == "web" ]]; then echo 99 else echo -e " ${INFO} No hosts file linked to dnsmasq, adding it in enabled state" fi # Add addn-host= to dnsmasq echo "addn-hosts=/etc/pihole/gravity.list" >> /etc/dnsmasq.d/01-pihole.conf restartDNS fi } tailFunc() { date=$(date +'%b %d ') echo -e " ${INFO} Press Ctrl-C to exit" tail -f /var/log/pihole.log | sed \ -e "s,\(${date}\| dnsmasq\[.*[0-9]]\),,g" \ -e "s,\(.*\(gravity.list\|black.list\| config \).* is \(${IPV4_ADDRESS%/*}\|${IPV6_ADDRESS:-NULL}\).*\),${COL_LIGHT_RED}&${COL_NC}," \ -e "s,.*\(query\[A\|DHCP\).*,${COL_NC}&${COL_NC}," \ -e "s,.*,${COL_DARK_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 ! timeout 2 nc -z tricorder.pi-hole.net 9998 &> /dev/null; 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 } 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" ) piholeStatus "$2";; "restartdns" ) restartDNS "$2";; "-a" | "admin" ) webpageFunc "$@";; "-t" | "tail" ) tailFunc;; "checkout" ) piholeCheckoutFunc "$@";; "tricorder" ) tricorderFunc;; * ) helpFunc;; esac