#!/usr/bin/env bash
#############################
## Issues ##
#############################
# If you experience any issues, please let me know here:
# https://github.com/goose-ws/bash-scripts
# These scripts are purely a passion project of convenience for myself, so pull requests welcome :)
#############################
## About ##
#############################
# This script serves to manage captive DNS on a UDM Pro. Captive DNS meaning it will *force*
# all clients to use a specific DNS server via iptables if they are not querying the DNS servers
# set by the ${allowedDNS} range. Useful for forcing devices with hard coded DNS to use your PiHole
# (For example, a Google Home) while being flexible enough to change that captive DNS destination
# if the host that *should* be serving it is not, for some reason.
# For reference, in my setup, my PiHole IP addresses are:
# - 10.10.10.10
# - 10.10.10.11
# They are the only two devices in that 10.10.10.0/24 CIDR range (other than the gateway, of course).
#############################
## Changelog ##
#############################
# 2024-07-24
# Rolled back the changes from the 2024-07-13 commit, as it appears the version of iptables used by
# the UDM-P does not support multiple comma-separated addresses when using the ! inverter
# Also added the '-w 30' flag to let iptables wait if it needs to
# 2024-07-13
# Added support for multiple allowed lookup ranges (See updated .env)
# 2024-01-27
# Improved some sanity checks and logic for escape scenarioes
# Added support for when a container has multiple networks attached (Multiple IP addresses)
# Updated the logic for sending Telegram messages to make sure the bot can authenticate to each channel
# Added support for super groups, silent messages (See updated .env file)
# Added support for sending error messages via telegram (See updated .env file)
# 2023-12-30
# Defined some paths, as iptables isn't in the default ${PATH} for sh
# Fixed some logic regarding persisting across restarts
# 2023-12-15
# Updated formatting and output to my "new standard"
# 2023-02-16
# Updated to work with Unifi 2.4.27, which is now based on Debian
# Greatly improved and simplified the logic of the script, now that we can use proper bash rather than sh
#############################
## Installation ##
#############################
# This script is meant for Unifi v2.4+, which is based on Debian. It will not work on older versions.
# It also depends on BoostChicken's Unifi Utilities: https://github.com/unifi-utilities/unifios-utilities
# This is so that you can run scripts in the '/data/on_boot.d/' directory on each boot.
# 0. Install BoostChicken's on-boot functionality to preserve across reboots.
# 1. Place this script at '/data/scripts/captive-dns.bash'
# 2. Set the script as executable (chmod +x).
# 3. Copy 'captive-dns.env.example' to 'captive-dns.env' and edit it to your liking
# 4. Run the command '/data/scripts/captive-dns.bash --install' to install it to cron and on_boot.d
#############################
## Sanity checks ##
#############################
if ! [ -e "/bin/bash" ]; then
echo "This script requires Bash"
exit 255
fi
if [[ -z "${BASH_VERSINFO[0]}" || "${BASH_VERSINFO[0]}" -lt "4" ]]; then
echo "This script requires Bash version 4 or greater"
exit 255
fi
# Define the ${PATH} for cron
PATH="/root/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
depArr=("awk" "cp" "curl" "date" "echo" "grep" "host" "md5sum" "realpath" "basename" "rm" "iptables" "sort")
depFail="0"
for i in "${depArr[@]}"; do
if [[ "${i:0:1}" == "/" ]]; then
if ! [[ -e "${i}" ]]; then
echo "${i}\\tnot found"
depFail="1"
fi
else
if ! command -v "${i}" > /dev/null 2>&1; then
echo "${i}\\tnot found"
depFail="1"
fi
fi
done
if [[ "${depFail}" -eq "1" ]]; then
echo "Dependency check failed"
exit 255
fi
realPath="$(realpath "${0}")"
scriptName="$(basename "${0}")"
lockFile="${realPath%/*}/.${scriptName}.lock"
# URL of where the most updated version of the script is
updateURL="https://raw.githubusercontent.com/goose-ws/bash-scripts/main/captive-dns.bash"
lineBreak="$(printf "\r\n\r\n")"
#############################
## Lockfile ##
#############################
if [[ -e "${lockFile}" ]]; then
if kill -s 0 "$(<"${lockFile}")" > /dev/null 2>&1; then
echo "${0##*/} :: $(date "+%Y-%m-%d %H:%M:%S") :: [1] Lockfile present, exiting"
exit 0
else
echo "${0##*/} :: $(date "+%Y-%m-%d %H:%M:%S") :: [1] Removing stale lockfile for PID $(<"${lockFile}")"
fi
fi
echo "${$}" > "${lockFile}"
#############################
## Standard Functions ##
#############################
function printOutput {
case "${1}" in
0) logLevel="[reqrd]";; # Required
1) logLevel="[error]";; # Errors
2) logLevel="[info] ";; # Informational
3) logLevel="[verb] ";; # Verbose
esac
if [[ "${1}" -le "${outputVerbosity}" ]]; then
echo "${0##*/} :: $(date "+%Y-%m-%d %H:%M:%S") :: ${logLevel} ${2}"
fi
}
function removeLock {
if rm -f "${lockFile}"; then
printOutput "3" "Lockfile removed"
else
printOutput "1" "Unable to remove lockfile"
fi
}
function badExit {
removeLock
if [[ -z "${2}" ]]; then
printOutput "0" "Received signal: ${1}"
exit "255"
else
if [[ "${telegramErrorMessages,,}" =~ ^(yes|true)$ ]]; then
sendTelegramMessage "${0##*/}${lineBreak}${lineBreak}Error Code ${1}:${lineBreak}${2}" "${telegramErrorChannel}"
fi
printOutput "1" "${2}"
exit "${1}"
fi
}
function cleanExit {
if [[ "${1}" == "silent" ]]; then
outputVerbosity="0"
fi
removeLock
exit 0
}
function sendTelegramMessage {
if [[ -n "${telegramBotId}" && -n "${telegramChannelId[0]}" ]]; then
# Message to send should be passed as function positional parameter #1
# We can pass an "Admin channel" as positional parameter #2 for the case of sending error messages
# Let's check to make sure our messaging credentials are valid
skipTelegram="0"
telegramOutput="$(curl -skL -H "Host: api.telegram.org" "https://${telegramAddr}/bot${telegramBotId}/getMe" 2>&1)"
curlExitCode="${?}"
if [[ "${curlExitCode}" -ne "0" ]]; then
printOutput "1" "Curl to Telegram to check Bot ID returned a non-zero exit code: ${curlExitCode}"
skipTelegram="1"
elif [[ -z "${telegramOutput}" ]]; then
printOutput "1" "Curl to Telegram to check Bot ID returned an empty string"
skipTelegram="1"
else
printOutput "3" "Curl exit code and null output checks passed"
fi
if [[ "${skipTelegram}" -eq "0" ]]; then
if ! [[ "$(jq -M -r ".ok" <<<"${telegramOutput,,}")" == "true" ]]; then
printOutput "1" "Telegram bot API check failed"
else
printOutput "2" "Telegram bot API key authenticated: $(jq -M -r ".result.username" <<<"${telegramOutput}")"
for chanId in "${telegramChannelId[@]}"; do
if [[ -n "${2}" ]]; then
chanId="${2}"
fi
telegramOutput="$(curl -skL -H "Host: api.telegram.org" "https://${telegramAddr}/bot${telegramBotId}/getChat?chat_id=${telegramChannelId}")"
curlExitCode="${?}"
if [[ "${curlExitCode}" -ne "0" ]]; then
printOutput "1" "Curl to Telegram to check channel returned a non-zero exit code: ${curlExitCode}"
elif [[ -z "${telegramOutput}" ]]; then
printOutput "1" "Curl to Telegram to check channel returned an empty string"
elif [[ "$(jq -M -r ".ok" <<<"${telegramOutput,,}")" == "true" ]]; then
printOutput "3" "Curl exit code and null output checks passed"
printOutput "2" "Telegram channel authenticated: $(jq -M -r ".result.title" <<<"${telegramOutput}")"
telegramOutput="$(curl -skL -H "Host: api.telegram.org" --data-urlencode "text=${1}" "https://${telegramAddr}/bot${telegramBotId}/sendMessage?chat_id=${chanId}&parse_mode=html" 2>&1)"
curlExitCode="${?}"
if [[ "${curlExitCode}" -ne "0" ]]; then
printOutput "1" "Curl to Telegram returned a non-zero exit code: ${curlExitCode}"
elif [[ -z "${telegramOutput}" ]]; then
printOutput "1" "Curl to Telegram to send message returned an empty string"
else
printOutput "3" "Curl exit code and null output checks passed"
# Check to make sure Telegram returned a true value for ok
if ! [[ "$(jq -M -r ".ok" <<<"${telegramOutput}")" == "true" ]]; then
printOutput "1" "Failed to send Telegram message:"
printOutput "1" ""
while read -r i; do
printOutput "1" "${i}"
done < <(jq . <<<"${telegramOutput}")
printOutput "1" ""
else
printOutput "2" "Telegram message sent successfully"
fi
fi
else
printOutput "1" "Telegram channel check failed"
fi
if [[ -n "${2}" ]]; then
break
fi
done
fi
fi
fi
}
#############################
## Unique Functions ##
#############################
removeRules () {
printOutput "3" "Initiating rule removal"
while read -r i; do
printOutput "2" "Removing iptables NAT rule ${i}"
iptables -w 30 -t nat -D PREROUTING "${i}"
done < <(iptables -w 30 -n -t nat -L PREROUTING --line-numbers | grep -E "to:([0-9]{1,3}[\.]){3}[0-9]{1,3}:53" | awk '{print $1}' | sort -nr)
printOutput "3" "Rule removal complete"
}
addRules () {
printOutput "3" "Initiating rule adding"
if ! [[ "${1}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
badExit "1" "${1} is not a valid IP address"
fi
if ! [[ "${2}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.([0-9]{1,3}|[0-9]/[0-9]{1,2})$ ]]; then
badExit "2" "${2} is not a valid IP address or CIDR range"
fi
# Should be passed as: addRules "IP address you want redirected to" "IP address or CIDR range allowed"
for intfc in "${vlanInterfaces[@]}"; do
printOutput "2" "Forcing interface ${intfc} to ${1}:${dnsPort}"
iptables -t nat -A PREROUTING -i "${intfc}" -p udp ! -s "${2}" ! -d "${2}" --dport "${dnsPort}" -j DNAT --to "${1}:${dnsPort}"
done
printOutput "3" "Rule adding complete"
}
testDNS () {
printOutput "3" "Initiating DNS test"
if ! host -W 5 "${testDomain}" "${1}" > /dev/null 2>&1; then
# Wait 5 seconds and try again, in case of timeout
printOutput "1" "DNS test attempt 1 failed: ${1}"
sleep 5
if ! host -W 5 "${testDomain}" "${1}" > /dev/null 2>&1; then
printOutput "1" "DNS test attempt 2 failed: ${1}"
printOutput "3" "DNS testing complete"
return 1
else
printOutput "2" "DNS test attempt 2 succeded: ${1}"
printOutput "3" "DNS testing complete"
return 0
fi
else
printOutput "2" "DNS test attempt 1 succeded: ${1}"
printOutput "3" "DNS testing complete"
return 0
fi
}
#############################
## Signal Traps ##
#############################
trap "badExit SIGINT" INT
trap "badExit SIGQUIT" QUIT
#############################
## Positional parameters ##
#############################
# We can run the positional parameter options without worrying about lockFile
case "${1,,}" in
"-h"|"--help")
echo "-h --help Displays this help message"
echo ""
echo "-u --Update Self Update to the most recent version"
echo ""
echo "-r --rules Displays current captive DNS rules"
echo ""
echo "-d --delete Deletes current captive DNS rules,"
echo " and does not replace them with anything"
echo ""
echo "-s --set Removes any rules which may exist, and"
echo " then sets new rules for captive DNS"
echo " Usage: -s <1> <2>"
echo " Where <1> is the captive DNS IP address"
echo " and <2> is the allowed DNS CIDR/IP"
echo ""
echo "--install Installs script to cron, executing it"
echo " once every minute. Also installs it to"
echo " the on_boot.d/ directory, to persist"
echo " across reboots"
echo ""
echo "--uninstall Removes the script from cron"
cleanExit "silent"
;;
"-u"|"--Update")
oldStartLine="0"
while read -r i; do
if [[ "${i}" == "## Changelog ##" ]]; then
oldStartLine="1"
elif [[ "${oldStartLine}" -eq "1" ]]; then
oldStartLine="2"
elif [[ "${oldStartLine}" -eq "2" ]]; then
oldStartLine="${i}"
break
fi
done < "${0}"
if curl -skL "${updateURL}" -o "${0}"; then
if chmod +x "${0}"; then
printOutput "1" "Update complete"
newStartLine="0"
while read -r i; do
if [[ "${newStartLine}" -eq "2" ]]; then
if [[ "${i}" == "${oldStartLine}" ]]; then
break
fi
if [[ "${i:2:1}" =~ ^[0-9]$ ]]; then
changelogArr+=(" ${i#\#}")
else
changelogArr+=(" - ${i#\#}")
fi
elif [[ "${newStartLine}" -eq "1" ]]; then
newStartLine="2"
elif [[ "${i}" == "## Changelog ##" ]]; then
newStartLine="1"
fi
done < <(curl -skL "${updateURL}")
printOutput "1" "Changelog:"
for i in "${changelogArr[@]}"; do
printOutput "1" "${i}"
done
cleanExit
else
badExit "3" "Update downloaded, but unable to \`chmod +x\`"
fi
else
badExit "4" "Unable to download Update"
fi
;;
"-r"|"--rules")
iptables -w 30 -n -t nat -L PREROUTING --line-numbers | grep -E "to:([0-9]{1,3}[\.]){3}[0-9]{1,3}:53"
cleanExit "silent"
;;
"-d"|"--delete")
removeRules;
cleanExit
;;
"-s"|"--set")
shift
removeRules;
addRules "${1}" "${2}";
cleanExit
;;
"--install")
echo "* * * * * root ${realPath%/*}/${scriptName}" > "/etc/cron.d/${scriptName%.bash}"
/etc/init.d/cron restart
echo "#!/bin/sh" > "/data/on_boot.d/10-captive-dns.sh"
echo "echo \"* * * * * root ${realPath%/*}/${scriptName}\" > \"/etc/cron.d/${scriptName%.bash}" >> "/data/on_boot.d/10-captive-dns.sh"
echo "/etc/init.d/cron restart" >> "/data/on_boot.d/10-captive-dns.sh"
chmod +x "/data/on_boot.d/10-captive-dns.sh"
cleanExit "silent"
;;
"--uninstall")
rm -f "/etc/cron.d/${scriptName%.bash}" "/data/on_boot.d/10-captive-dns.sh"
/etc/init.d/cron restart
cleanExit "silent"
;;
esac
#############################
## Initiate .env file ##
#############################
if [[ -e "${realPath%/*}/${scriptName%.bash}.env" ]]; then
source "${realPath%/*}/${scriptName%.bash}.env"
else
badExit "5" "Error: \"${realPath%/*}/${scriptName%.bash}.env\" does not appear to exist"
fi
varFail="0"
# Standard checks
if ! [[ "${updateCheck,,}" =~ ^(yes|no|true|false)$ ]]; then
echo "${0##*/} :: $(date "+%Y-%m-%d %H:%M:%S") :: [1] Option to check for updates not valid. Assuming no."
updateCheck="No"
fi
if ! [[ "${outputVerbosity}" =~ ^[1-3]$ ]]; then
echo "${0##*/} :: $(date "+%Y-%m-%d %H:%M:%S") :: [1] Invalid output verbosity defined. Assuming level 1 (Errors only)"
outputVerbosity="1"
fi
# Config specific checks
if ! [[ "${allowedDNS}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.([0-9]{1,3}|[0-9]/[0-9]{1,2})$ ]]; then
printOutput "1" "Allowed DNS (${allowedDNS}) is not a valid IP address or CIDR range"
varFail="1"
fi
if ! [[ "${primaryDNS}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
printOutput "1" "Primary DNS (${primaryDNS}) is not a valid IP address"
varFail="1"
fi
if [[ -z "${secondaryDNS}" ]]; then
printOutput "1" "No Secondary DNS defined, falling back to Tertiary DNS"
secondaryDNS="${tertiaryDNS}"
elif ! [[ "${secondaryDNS}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
printOutput "1" "Secondary DNS (${secondaryDNS}) is not a valid IP address"
varFail="1"
fi
if ! [[ "${tertiaryDNS}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
printOutput "1" "Tertiary DNS (${tertiaryDNS}) is not a valid IP address"
varFail="1"
fi
if ! [[ "${dnsPort}" =~ ^[0-9]{1,5}$ ]]; then
printOutput "1" "DNS Port (${dnsPort}) is not a valid port number"
varFail="1"
fi
if ! [[ "${testDomain}" =~ ^([a-z0-9]{1,60}\.)+[a-z]{2,3}$ ]]; then
printOutput "1" "Address to test DNS (${testDomain}) is not a valid domain"
varFail="1"
fi
if [[ "${#vlanInterfaces[@]}" -eq "0" ]]; then
printOutput "1" "No VLAN interfaces defined"
varFail="1"
else
for i in "${vlanInterfaces[@]}"; do
if ! [[ "${i}" =~ ^br[0-9]+$ ]]; then
printOutput "1" "${i} does not appear to be a valid VLAN interface"
varFail="1"
fi
done
fi
# Quit if failures
if [[ "${varFail}" -eq "1" ]]; then
badExit "6" "Please fix above errors"
fi
#############################
## Update check ##
#############################
if [[ "${updateCheck,,}" =~ ^(yes|true)$ ]]; then
newest="$(curl -skL "${updateURL}" | md5sum | awk '{print $1}')"
current="$(md5sum "${0}" | awk '{print $1}')"
if ! [[ "${newest}" == "${current}" ]]; then
printOutput "0" "A newer version is available"
# If our ${TERM} is dumb, we're probably running via cron, and should push a message to Telegram, if allowed
if [[ "${TERM,,}" == "dumb" ]]; then
if [[ "${telegramErrorMessages}" =~ ^(yes|true)$ ]]; then
sendTelegramMessage "[${0##*/}] An update is available" "${telegramErrorChannel}"
fi
fi
else
printOutput "3" "No new updates available"
fi
fi
#############################
## Payload ##
#############################
printOutput "2" "Verifying internet connectivity"
if ! ping -w 5 -c 1 "${tertiaryDNS}" > /dev/null 2>&1; then
# It appears that it is not
badExit "7" "Internet appears to be offline"
else
printOutput "3" "Internet connectivity verified"
fi
# We read this into an array as a cheap way of counting the number of results. It should only be zero or one.
readarray -t captiveDNS < <(iptables -w 30 -n -t nat --list PREROUTING | grep -Eo "to:([0-9]{1,3}[\.]){3}[0-9]{1,3}:53" | grep -Eo "([0-9]{1,3}[\.]){3}[0-9]{1,3}" | sort -u)
if [[ "${#captiveDNS[@]}" -eq "0" ]]; then
# No rules are set
printOutput "3" "No DNS rules detected"
if testDNS "${primaryDNS}"; then
# Primary test succeded
addRules "${primaryDNS}" "${allowedDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from null [None set] to primary [${primaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Primary test failed. Test secondary.
if testDNS "${secondaryDNS}"; then
# Secondary test succeded
addRules "${secondaryDNS}" "${secondaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from null [None set] to secondary [${secondaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Secondary test failed
addRules "${tertiaryDNS}" "${tertiaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from null [None set] to tertiary [${tertiaryDNS}]"
sendTelegramMessage "${eventText}"
fi
fi
elif [[ "${#captiveDNS[@]}" -ge "2" ]]; then
# There is more than one result. This should not happen.
badExit "0" "More than one captive DNS entry found";
elif [[ "${captiveDNS[0]}" == "${primaryDNS}" ]]; then
# Captive DNS is Primary
printOutput "3" "Captive DNS is Primary DNS: ${primaryDNS}"
printOutput "2" "Testing Primary DNS server: ${primaryDNS}"
if testDNS "${primaryDNS}"; then
# Primary test succeded
printOutput "2" "Primary DNS check succeeded"
cleanExit
else
# Primary test failed. Test secondary.
printOutput "1" "Primary DNS check failed"
printOutput "2" "Testing Secondary DNS server: ${secondaryDNS}"
if testDNS "${secondaryDNS}"; then
# Secondary test succeded
printOutput "2" "Secondary DNS check succeeded"
removeRules;
addRules "${secondaryDNS}" "${secondaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from primary [${primaryDNS}] to secondary [${secondaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Secondary test failed
printOutput "1" "Secondary DNS check failed"
removeRules;
addRules "${tertiaryDNS}" "${tertiaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from primary [${primaryDNS}] to tertiary [${tertiaryDNS}]"
sendTelegramMessage "${eventText}"
fi
fi
elif [[ "${captiveDNS[0]}" == "${secondaryDNS}" ]]; then
# Captive DNS is Secondary
printOutput "3" "Captive DNS is Secondary DNS: ${secondaryDNS}"
printOutput "2" "Testing Primary DNS server: ${primaryDNS}"
if testDNS "${primaryDNS}"; then
# Primary test succeded
printOutput "2" "Primary DNS check succeeded"
removeRules;
addRules "${primaryDNS}" "${allowedDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from secondary [${secondaryDNS}] to primary [${primaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Primary test failed. Test secondary.
printOutput "1" "Primary DNS check failed"
printOutput "2" "Testing Secondary DNS server: ${secondaryDNS}"
if testDNS "${secondaryDNS}"; then
# Secondary test succeded
printOutput "2" "Secondary DNS check succeeded"
cleanExit
else
# Secondary test failed
printOutput "1" "Secondary DNS check failed"
removeRules;
addRules "${tertiaryDNS}" "${tertiaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from secondary [${secondaryDNS}] to tertiary [${tertiaryDNS}]"
sendTelegramMessage "${eventText}"
fi
fi
elif [[ "${captiveDNS[0]}" == "${tertiaryDNS}" ]]; then
# Captive DNS is Tertiary
printOutput "3" "Captive DNS is Tertiary DNS: ${tertiaryDNS}"
printOutput "2" "Testing Primary DNS server: ${primaryDNS}"
if testDNS "${primaryDNS}"; then
# Primary test succeded
printOutput "2" "Primary DNS check succeeded"
removeRules;
addRules "${primaryDNS}" "${allowedDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from tertiary [${tertiaryDNS}] to primary [${primaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Primary test failed. Test secondary.
printOutput "1" "Primary DNS check failed"
printOutput "2" "Testing Secondary DNS server: ${secondaryDNS}"
if testDNS "${secondaryDNS}"; then
# Secondary test succeded
printOutput "2" "Secondary DNS check succeeded"
removeRules;
addRules "${secondaryDNS}" "${secondaryDNS}";
eventText="Captive DNS Status Change${lineBreak}Captive DNS switched from tertiary [${tertiaryDNS}] to secondary [${secondaryDNS}]"
sendTelegramMessage "${eventText}"
else
# Secondary test failed
printOutput "1" "Secondary DNS check failed"
cleanExit
fi
fi
else
# One set of rules exist, but it's not Primary, Secondary, or Tertiary - We should never reach this
badExit "0" "Rules do not match any known DNS server: ${captiveDNS[0]}";
fi
#############################
## End of file ##
#############################
cleanExit