#!/bin/bash
# vim: cindent:shiftwidth=4:tabstop=4:smarttab:textwidth=100

#set -o posix
set -o errexit
set -o pipefail
set -o nounset
#set -o xtrace

#$title$ Internet Domain Name System (DNS) recursive resolvers
#$check$ all recursive servers defined in /etc/resolv.conf work OK
#$ref$ man resolv.conf
#$author$ Rafal Rzeczkowski
#$version$ 1.0.4

level_check long

#CHANGELOG
#0.50	initial
#0.60	flag the use of public nameservers
#0.61	bash <4 version compatibility - string lowercase
#0.62	bash 4.3 version compatibility - use += for array additions
#0.63	metadata headers, help messages
#0.64	added Neustar DNS Advantage public Recursive DNS
#0.65	retry lookups $RETRIES times when using UDP
#0.66	test using the TCP protocol too
#0.67	use is_element_of() from syscheck 0.68
#0.68	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.69	query the DNS server with 'dig' instead of 'host'
#0.70	update public nameserver list
#0.71	permit sole nameserver when it is also the default IPv4 gateway
#0.72	continue processing when IPv4 default gateway is missing
#0.73	skip IPv4 nameservers when node has no IPv4 connectivity
#0.74	publish performance counters
#0.75	update public nameserver blacklist
#0.8.0	restyle according to https://kb.clearcable.ca/KB/ProgrammingStyleStandards
#0.8.1	clear errexit on "read -a" for compatibility with bash 3.2.39
#0.9.0	validate DNSSEC recursive functionality
#1.0.0	enumerate resolvers from systemd-resolved
#1.0.1	extend timeout for a negative DNSSEC response to 20 seconds
#1.0.2	move sd_booted() function to syscheck dispatcher
#1.0.3	probe IPv4 activation status based on route presence
#1.0.4	skip DNSSEC tests when validation support is turned off in systemd
#1.0.5	flag public nameservers as `caution` instead of `warning`

declare -r CCN_DNS_QUERY_TARGET='syscheck.test.clearcable.net'
declare -r RESOLV_CONF_FILE='/etc/resolv.conf'
declare -r -i MAXNS=3		# defined in resolv.h
declare -r DNSSEC_POSITIVE_TARGET="$CCN_DNS_QUERY_TARGET"
declare -r DNSSEC_NEGATIVE_TARGET='www.dnssec-failed.org'
declare -r -i DIG_TIMEOUT=20

declare -r -a PUBLIC_NAMESERVERS=(
	#https://developers.google.com/speed/public-dns/docs/using
	8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844

	#https://developers.google.com/speed/public-dns/docs/dns64
	2001:4860:4860::6464 2001:4860:4860::64

	#https://www.opendns.com/setupguide/
	#https://www.opendns.com/about/innovations/ipv6/
	208.67.222.222 208.67.220.220 2620:119:35::35 2620:119:53::53

	#https://www.quad9.net/faq/
	9.9.9.9 149.112.112.112 2620:fe::fe 2620:fe::9

	#https://1.1.1.1/dns/
	1.1.1.1 1.0.0.1 2606:4700:4700::1111 2606:4700:4700::1001

	#https://cleanbrowsing.org/filters
	#Family Filter
	185.228.168.168 185.228.169.168 2a0d:2a00:1:: 2a0d:2a00:2::
	#Adult Filter
	185.228.168.10 185.228.169.11 2a0d:2a00:1::1 2a0d:2a00:2::1
	#Security Filter
	185.228.168.9 185.228.169.9 2a0d:2a00:1::2 2a0d:2a00:2::2

	#https://alternate-dns.com/
	76.76.19.19 76.223.122.150 2602:fcbc::ad 2602:fcbc:2::ad

	#https://adguard-dns.io/en/public-dns.html
	#Default servers
	94.140.14.14 94.140.15.15 2a10:50c0::ad1:ff 2a10:50c0::ad2:ff
	#Non-filtering servers
	94.140.14.140 94.140.14.141 2a10:50c0::1:ff 2a10:50c0::2:ff
	#Family protection servers
	94.140.14.15 94.140.15.16 2a10:50c0::bad1:ff 2a10:50c0::bad2:ff

	#https://www.publicdns.xyz/public/verisign.html
	64.6.64.6 64.6.65.6 2620:74:1b::1:1 2620:74:1c::2:2

	#https://www.publicdns.neustar/
	#Unfiltered Resolution
	64.6.64.6 64.6.65.6 2620:74:1b::1:1 2620:74:1c::2:2
	#Threat Protection
	156.154.70.2 156.154.71.2 2610:a1:1018::2 2610:a1:1019::2
	#Family Secure
	156.154.70.3 156.154.71.3 2610:a1:1018::3 2610:a1:1019::3

	#https://dns.watch/index
	84.200.69.80 84.200.70.40 2001:1608:10:25::1c04:b12f 2001:1608:10:25::9249:d69b

	#https://www.comodo.com/secure-dns/switch/
	8.26.56.26 8.20.247.20

	#https://www.centurylink.com/home/help/internet/dns.html
	#Dynamic IP Customers
	205.171.3.66 205.171.202.166
	#Static IP Customers
	205.171.3.26 205.171.2.26

	#https://www.safedns.com/en/guides/win10/
	195.46.39.39 195.46.39.40

	#https://servers.opennic.org/
	134.195.4.2 2604:ffc0::
	103.1.206.179 2400:c400:1002:11:fed:bee0:4433:6fb0
	216.238.104.56 2001:19f0:b800:1556::53
	159.89.120.99
	51.89.88.77 2001:41d0:700:1174::
	94.247.43.254 2a00:f826:8:1::254
	195.10.195.195 2a00:f826:8:2::195
	130.61.117.123 2603:c020:8002:31ff::2000
	89.163.140.67 2001:4ba0:ffa4:1ce::
	88.198.92.222 2a01:4f8:1c0c:82c0::1
	194.36.144.87 2a03:4000:4d:c92:88c0:96ff:fec6:b9d
	130.61.64.122
	130.61.69.123
	51.89.88.77 2001:41d0:700:1174::
	94.247.43.254 2a00:f826:8:1::254
	89.163.140.67 2001:4ba0:ffa4:1ce::
	51.158.108.203
	51.158.108.203 2001:470:1f15:b80::53
	95.179.226.37 2001:19f0:7402:1840::53
	192.71.166.92 2a03:f80:30:192:71:166:92:1
	79.133.199.87 2001:470:70:4d6::2
	51.83.172.84
	144.24.181.253

	#https://help.dyn.com/internet-guide-setup/
	216.146.35.35 216.146.36.36

	#https://dns.yandex.com/advanced/
	#Basic
	77.88.8.8 77.88.8.1 2a02:6b8::feed:0ff 2a02:6b8:0:1::feed:0ff
	#Safe
	77.88.8.88 77.88.8.2 2a02:6b8::feed:bad 2a02:6b8:0:1::feed:bad
	#Family
	77.88.8.7 77.88.8.3 2a02:6b8::feed:a11 2a02:6b8:0:1::feed:a11

	#https://controld.com/free-dns/
	#Unfiltered
	76.76.2.0 76.76.10.0 2606:1a40:: 2606:1a40:1::
	#Malware
	76.76.2.1 76.76.10.1 2606:1a40::1 2606:1a40:1::1
	#Ads & Tracking
	76.76.2.2 76.76.10.2 2606:1a40::2 2606:1a40:1::2
	#Social
	76.76.2.3 76.76.10.3 2606:1a40::3 2606:1a40:1::3
	#Family Friendly
	76.76.2.4 76.76.10.4 2606:1a40::4 2606:1a40:1::4
	#Uncensored
	76.76.2.5 76.76.10.5 2606:1a40::5 2606:1a40:1::5
)
#echodebug "${PUBLIC_NAMESERVERS[@]}"

IPV4_ACTIVE=$(ip -oneline -family inet route show)

if [[ ! -s $RESOLV_CONF_FILE ]]; then
	fail critical "missing resolver configuration file $RESOLV_CONF_FILE"
	helpmsg 'all system must have the {/etc/resolv.conf} file; check Bcfg2 site profile layout'
	exit
fi

declare -a nameservers
if sd_booted; then
	IFS=$'\n' read -d '' -r -a nameservers_aggregate < <(COLUMNS=200 resolvectl status | awk '{
if ($1=="Link")
	link=substr($3,2,length($3)-2)

dns_field=0
if ($1" "$2" "$3=="Fallback DNS Servers" || $1" "$2" "$3=="Current DNS Server:") {
	dns_field=4
} else if ($1" "$2=="DNS Servers:") {
	dns_field=3
}

if (dns_field)
	for (i=dns_field;i<=NF;i++)
		if (substr($(i),1,4)=="fe80")
			print $(i)"%"link
		else
			print $(i)
}')
	declare -A nameservers_unique
	for nameserver in "${nameservers_aggregate[@]}"; do
		nameservers_unique[$nameserver]=1
	done
	read -r -a nameservers <<< "${!nameservers_unique[@]}"
else
	IFS=$'\n' read -d '' -r -a nameservers < <(awk \
		'{if ($1=="nameserver"){print $2}}' "$RESOLV_CONF_FILE") || true
fi
#echodebug "${nameservers[@]}"

if [[ ${#nameservers[@]} -eq 0 ]]; then
	fail critical 'no nameservers configured'
	exit
elif [[ ${#nameservers[@]} -eq 1 ]]; then
	IFS=$'\n' read -d '' -r -a DEFAULT4_NH < <(ip -oneline -family inet route show |
		awk '{if ($1=="default"){print $3;exit}}') || true
	if [[ ${#DEFAULT4_NH[@]} -eq 1 && ${nameservers[0]} = "${DEFAULT4_NH[0]}" ]]; then
		echodebug 'IPv4 default gateway is the sole nameserver'
	else
		fail warning 'no redundant nameservers configured'
		exit
	fi
elif [[ ${#nameservers[@]} -le 3 ]]; then
	echodebug "${#nameservers[@]} nameservers"
elif sd_booted; then
	# no limit enforced for systemd-resolved
	echodebug "${#nameservers[@]} nameservers"
elif [[ ${#nameservers[@]} -gt $MAXNS ]]; then
	WARNING="too many (${#nameservers[@]}) nameserver definitions found in $RESOLV_CONF_FILE, "
	WARNING+="expected a maximum of MAXNS=$MAXNS as per resolv.h"
	fail warning "$WARNING"
	exit
fi
performance nameservers ${#nameservers[@]}

declare -a ok=()
declare -a bad=()
declare -a valid=()
for nameserver in "${nameservers[@]}"; do
	if [[ -z "$IPV4_ACTIVE" ]]; then
		if [[ "$nameserver" != "${nameserver#*.}" ]]; then
			echodebug "skipping IPv4 resolver {$nameserver} in an IPv6-only environment"
			continue
		fi
	fi
	valid+=("$nameserver")

	for query_type in 'A' 'AAAA'; do
		for protocol_type in 'UDP' 'TCP'; do
			case $protocol_type in
				TCP) PROTOCOL_FLAG='+tcp';;
				UDP) PROTOCOL_FLAG='+notcp';;
			esac
			if dig @"$nameserver" -t $query_type -q "$CCN_DNS_QUERY_TARGET." +short +noanswer $PROTOCOL_FLAG; then
				ok+=("$nameserver/$protocol_type/$query_type")
			else
				bad+=("$nameserver/$protocol_type/$query_type")
			fi
		done
	done
done

if [[ ${#ok[@]} -eq 0 ]]; then
	fail critical "no functional resolvers found in $RESOLV_CONF_FILE"
	helpmsg 'check IP connectivity to the nameservers'
	exit
elif [[ ${#bad[@]} -gt 0 ]]; then
	fail warning "failed to respond to {$CCN_DNS_QUERY_TARGET} query: ${bad[*]}"
	helpmsg 'check routing to the nameservers (IPv4/IPv6); check ACL definitions in {/etc/bind/named.conf.acl}'
	exit
else
	echodebug "all ${#ok[@]} queries = (A,AAAA)*(UDP,TCP)*(${#nameservers[@]} nameservers) worked as expected"
fi

for nameserver in "${nameservers[@]}"; do
	##nameserver_lowercase=${nameserver,,} # requires bash version 4+
	nameserver_lowercase=$(echo "$nameserver"|tr '[:upper:]' '[:lower:]')
	if is_element_of "$nameserver_lowercase" "${PUBLIC_NAMESERVERS[@]}"; then
		fail caution "public nameserver found: $nameserver"
		helpmsg 'use only site-local or Clearcable nameservers'
		exit
	fi
done

dnssec_validation_support=1
if sd_booted; then
	dnssec_validation_support=$(COLUMNS=200 resolvectl status |
		grep --count --fixed-strings 'DNSSEC=yes')
fi
if [[ $dnssec_validation_support -gt 0 ]]; then
echodebug 'DNSSEC validation support:'
for nameserver in "${valid[@]}"; do
	#+
	query_spec=("$DNSSEC_POSITIVE_TARGET." 'AAAA')
	read -r -a dns_response_flags < <(dig +recurse +dnssec "${query_spec[@]}" @"$nameserver"|
		awk -F';; flags: ' '{if ($2){flag_end=index($2,";");print substr($2,1,flag_end-1)}}') || true

	if ! is_element_of 'ad' "${dns_response_flags[@]}"; then
		fail caution "$nameserver failed to validate {${query_spec[0]}} via DNSSEC"
		helpmsg 'ensure that DNSSEC compliant recursive servers are being used'
		exit
	fi

	#-
	query_spec=("$DNSSEC_NEGATIVE_TARGET." 'A')
	response=$(dig +timeout=$DIG_TIMEOUT +recurse +short "${query_spec[@]}" @"$nameserver")
	if [[ -n "$response" ]]; then
		fail caution "$nameserver responds to queries for a failed DNSSEC domain {${query_spec[0]}}"
		helpmsg 'ensure that DNSSEC compliant recursive servers are being used'
		exit
	fi

	echodebug "$nameserver +/- ok"
done
fi

ok
