#!/bin/sh
# Begin $rc_base/init.d/unbound

# Description : Unbound DNS resolver boot script for IPfire
# Author      : Marcel Lorenz <marcel.lorenz@ipfire.org>

. /etc/sysconfig/rc
. ${rc_functions}

TEST_DOMAIN="ipfire.org"

# This domain will never validate
TEST_DOMAIN_FAIL="dnssec-failed.org"

INSECURE_ZONES=
USE_FORWARDERS=1
ENABLE_SAFE_SEARCH=off

# Cache any local zones for 60 seconds
LOCAL_TTL=60

# EDNS buffer size
EDNS_DEFAULT_BUFFER_SIZE=4096

# Load optional configuration
[ -e "/etc/sysconfig/unbound" ] && . /etc/sysconfig/unbound

ip_address_revptr() {
	local addr=${1}

	local a1 a2 a3 a4
	IFS=. read -r a1 a2 a3 a4 <<< ${addr}

	echo "${a4}.${a3}.${a2}.${a1}.in-addr.arpa"
}

read_name_servers() {
	local i
	for i in 1 2; do
		echo "$(</var/ipfire/red/dns${i})"
	done 2>/dev/null | xargs echo
}

check_red_has_carrier_and_ip() {
	# Interface configured ?
	[ ! -e "/var/ipfire/red/iface" ] && return 0;

	# Interface present ?
	[ ! -e "/sys/class/net/$(</var/ipfire/red/iface)" ] && return 0;

	# has carrier ?
	[ ! "$(</sys/class/net/$(</var/ipfire/red/iface)/carrier)" = "1" ] && return 0;

	# has ip ?
	[ "$(ip address show dev $(</var/ipfire/red/iface) | grep "inet")" = "" ] && return 0;

	return 1;
}

config_header() {
	echo "# This file is automatically generated and any changes"
	echo "# will be overwritten. DO NOT EDIT!"
	echo
}

update_forwarders() {
	check_red_has_carrier_and_ip
	if [ "${USE_FORWARDERS}" = "1" -a "${?}" = "1" ]; then
		local forwarders
		local broken_forwarders

		local ns
		for ns in $(read_name_servers); do
			test_name_server ${ns} &>/dev/null
			case "$?" in
				# Only use DNSSEC-validating or DNSSEC-aware name servers
				0|2)
					forwarders="${forwarders} ${ns}"
					;;
				*)
					broken_forwarders="${broken_forwarders} ${ns}"
					;;
			esac
		done

		# Determine EDNS buffer size
		local new_edns_buffer_size=${EDNS_DEFAULT_BUFFER_SIZE}

		for ns in ${forwarders}; do
			local edns_buffer_size=$(ns_determine_edns_buffer_size ${ns})
			if [ -n "${edns_buffer_size}" ]; then
				if [ ${edns_buffer_size} -lt ${new_edns_buffer_size} ]; then
					new_edns_buffer_size=${edns_buffer_size}
				fi
			fi
		done

		if [ ${new_edns_buffer_size} -lt ${EDNS_DEFAULT_BUFFER_SIZE} ]; then
			boot_mesg "EDNS buffer size reduced to ${new_edns_buffer_size}" ${WARNING}
			echo_warning

			unbound-control -q set_option edns-buffer-size: ${new_edns_buffer_size}
		fi

		# Show warning for any broken upstream name servers
		if [ -n "${broken_forwarders}" ]; then
			boot_mesg "Ignoring broken upstream name server(s): ${broken_forwarders:1}" ${WARNING}
			echo_warning
		fi

		if [ -n "${forwarders}" ]; then
			boot_mesg "Configuring upstream name server(s): ${forwarders:1}" ${INFO}
			echo_ok

			# Make sure DNSSEC is activated
			enable_dnssec

			echo "${forwarders}" > /var/ipfire/red/dns
			unbound-control -q forward ${forwarders}
			return 0

		# In case we have found no working forwarders
		else
			# Test if the recursor mode is available
			if can_resolve_root +bufsize=${new_edns_buffer_size}; then
				# Make sure DNSSEC is activated
				enable_dnssec

				boot_mesg "Falling back to recursor mode" ${WARNING}
				echo_warning

			# If not, we set DNSSEC in permissive mode and allow using all recursors
			elif [ -n "${broken_forwarders}" ]; then
				disable_dnssec

				boot_mesg "DNSSEC has been set to permissive mode" ${FAILURE}
				echo_failure

				echo "${broken_forwarders}" > /var/ipfire/red/dns
				unbound-control -q forward ${broken_forwarders}
				return 0
			fi
		fi
	fi

	# If forwarders cannot be used we run in recursor mode
	echo "local recursor" > /var/ipfire/red/dns
	unbound-control -q forward off
}

remove_forwarders() {
	enable_dnssec
	echo "local recursor" > /var/ipfire/red/dns
	unbound-control -q forward off

}

own_hostname() {
	local hostname=$(hostname -f)
	# 1.1.1.1 is reserved for unused green, skip this
	if [ -n "${GREEN_ADDRESS}" -a "${GREEN_ADDRESS}" != "1.1.1.1" ]; then
		unbound-control -q local_data "${hostname} ${LOCAL_TTL} IN A ${GREEN_ADDRESS}"
	fi

	local address
	for address in ${GREEN_ADDRESS} ${BLUE_ADDRESS} ${ORANGE_ADDRESS}; do
		[ -n "${address}" ] || continue
		[ "${address}" = "1.1.1.1" ] && continue

		address=$(ip_address_revptr ${address})
		unbound-control -q local_data "${address} ${LOCAL_TTL} IN PTR ${hostname}"
	done
}

update_hosts() {
	local enabled address hostname domainname generateptr

	while IFS="," read -r enabled address hostname domainname generateptr; do
		[ "${enabled}" = "on" ] || continue

		# Build FQDN
		local fqdn="${hostname}.${domainname}"

		unbound-control -q local_data "${fqdn} ${LOCAL_TTL} IN A ${address}"

		# Skip reverse resolution if the address equals the GREEN address
		[ "${address}" = "${GREEN_ADDRESS}" ] && continue

		# Skip reverse resolution if user requested not to do so
		[ "${generateptr}" = "off" ] && continue

		# Add RDNS
		address=$(ip_address_revptr ${address})
		unbound-control -q local_data "${address} ${LOCAL_TTL} IN PTR ${fqdn}"
	done < /var/ipfire/main/hosts
}

write_forward_conf() {
	(
		config_header

		local insecure_zones="${INSECURE_ZONES}"

		local enabled zone server servers remark disable_dnssec rest
		while IFS="," read -r enabled zone servers remark disable_dnssec rest; do
			# Line must be enabled.
			[ "${enabled}" = "on" ] || continue

			# Zones that end with .local are commonly used for internal
			# zones and therefore not signed
			case "${zone}" in
				*.local)
					insecure_zones="${insecure_zones} ${zone}"
					;;
				*)
					if [ "${disable_dnssec}" = "on" ]; then
						insecure_zones="${insecure_zones} ${zone}"
					fi
					;;
			esac

			# Reverse-lookup zones must be stubs
			case "${zone}" in
				*.in-addr.arpa)
					echo "stub-zone:"
					echo "	name: ${zone}"
					for server in ${servers//|/ }; do
						if [[ ${server} =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
							echo "	stub-addr: ${server}"
						else
							echo "	stub-host: ${server}"
						fi
					done
					echo
					echo "server:"
					echo "	local-zone: \"${zone}\" transparent"
					echo
					;;
				*)
					echo "forward-zone:"
					echo "	name: ${zone}"
					for server in ${servers//|/ }; do
						if [[ ${server} =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
							echo "	forward-addr: ${server}"
						else
							echo "	forward-host: ${server}"
						fi
					done
					echo
					;;
			esac
		done < /var/ipfire/dnsforward/config

		if [ -n "${insecure_zones}" ]; then
			echo "server:"

			for zone in ${insecure_zones}; do
				echo "	domain-insecure: ${zone}"
			done
		fi
	) > /etc/unbound/forward.conf
}

write_tuning_conf() {
	# https://www.unbound.net/documentation/howto_optimise.html

	# Determine number of online processors
	local processors=$(getconf _NPROCESSORS_ONLN)

	# Determine number of slabs
	local slabs=1
	while [ ${slabs} -lt ${processors} ]; do
		slabs=$(( ${slabs} * 2 ))
	done

	# Determine amount of system memory
	local mem=$(get_memory_amount)

	# In the worst case scenario, unbound can use double the
	# amount of memory allocated to a cache due to malloc overhead

	# Even larger systems with more than 8GB of RAM
	if [ ${mem} -ge 8192 ]; then
		mem=1024

	# Extra large systems with more than 4GB of RAM
	elif [ ${mem} -ge 4096 ]; then
		mem=512

	# Large systems with more than 2GB of RAM
	elif [ ${mem} -ge 2048 ]; then
		mem=256

	# Medium systems with more than 1GB of RAM
	elif [ ${mem} -ge 1024 ]; then
		mem=128

	# Small systems with less than 256MB of RAM
	elif [ ${mem} -le 256 ]; then
		mem=16

	# Everything else
	else
		mem=64
	fi

	(
		config_header

		# We run one thread per processor
		echo "num-threads: ${processors}"
		echo "so-reuseport: yes"

		# Adjust number of slabs
		echo "infra-cache-slabs: ${slabs}"
		echo "key-cache-slabs: ${slabs}"
		echo "msg-cache-slabs: ${slabs}"
		echo "rrset-cache-slabs: ${slabs}"

		# Slice up the cache
		echo "rrset-cache-size: $(( ${mem} / 2 ))m"
		echo "msg-cache-size: $(( ${mem} / 4 ))m"
		echo "key-cache-size: $(( ${mem} / 4 ))m"

		# Increase parallel queries
		echo "outgoing-range: 8192"
		echo "num-queries-per-thread: 4096"

		# Use larger send/receive buffers
		echo "so-sndbuf: 4m"
		echo "so-rcvbuf: 4m"
	) > /etc/unbound/tuning.conf
}

get_memory_amount() {
	local key val unit

	while read -r key val unit; do
		case "${key}" in
			MemTotal:*)
				# Convert to MB
				echo "$(( ${val} / 1024 ))"
				break
				;;
		esac
	done < /proc/meminfo
}

test_name_server() {
	local ns=${1}
	local args

	# Return codes:
	# 0	DNSSEC validating
	# 1	Error: unreachable, etc.
	# 2	DNSSEC aware
	# 3	NOT DNSSEC-aware

	# Exit when the server is not reachable
	ns_is_online ${ns} || return 1

	# Determine the maximum edns buffer size that works
	local edns_buffer_size=$(ns_determine_edns_buffer_size ${ns})
	if [ -n "${edns_buffer_size}" ]; then
		args="${args} +bufsize=${edns_buffer_size}"
	fi

	local errors
	for rr in DNSKEY DS RRSIG; do
		if ! ns_forwards_${rr} ${ns} ${args}; then
			errors="${errors} ${rr}"
		fi
	done

	if [ -n "${errors}" ]; then
		echo >&2 "Unable to retrieve the following resource records from ${ns}: ${errors:1}"
		return 3
	fi

	if ns_is_validating ${ns} ${args}; then
		# Return 0 if validating
		return 0
	else
		# Is DNSSEC-aware
		return 2
	fi
}

# Sends an A query to the nameserver w/o DNSSEC
ns_is_online() {
	local ns=${1}
	shift

	dig @${ns} +nodnssec A ${TEST_DOMAIN} $@ >/dev/null
}

# Resolving ${TEST_DOMAIN_FAIL} will fail if the nameserver is validating
ns_is_validating() {
	local ns=${1}
	shift

	if ! dig @${ns} A ${TEST_DOMAIN_FAIL} $@ | grep -q SERVFAIL; then
		return 1
	else
		# Determine if NS replies with "ad" data flag if DNSSEC enabled
		dig @${ns} +dnssec SOA ${TEST_DOMAIN} $@ | awk -F: '/\;\;\ flags\:/ { s=1; if (/\ ad/) s=0; exit s }'
	fi
}

# Checks if we can retrieve the DNSKEY for this domain.
# dig will print the SOA if nothing was found
ns_forwards_DNSKEY() {
	local ns=${1}
	shift

	dig @${ns} DNSKEY ${TEST_DOMAIN} $@ | grep -qv SOA
}

ns_forwards_DS() {
	local ns=${1}
	shift

	dig @${ns} DS ${TEST_DOMAIN} $@ | grep -qv SOA
}

ns_forwards_RRSIG() {
	local ns=${1}
	shift

	dig @${ns} +dnssec A ${TEST_DOMAIN} $@ | grep -q RRSIG
}

ns_supports_tcp() {
	local ns=${1}
	shift

	dig @${ns} +tcp A ${TEST_DOMAIN} $@ >/dev/null || return 1
}

ns_determine_edns_buffer_size() {
	local ns=${1}
	shift

	local b
	for b in 4096 2048 1500 1480 1464 1400 1280 512; do
		if dig @${ns} +dnssec +bufsize=${b} A ${TEST_DOMAIN} $@ >/dev/null; then
			echo "${b}"
			return 0
		fi
	done

	return 1
}

get_root_nameservers() {
	while read -r hostname ttl record address; do
		# Searching for A records
		[ "${record}" = "A" ] || continue

		echo "${address}"
	done < /etc/unbound/root.hints
}

can_resolve_root() {
	local ns
	for ns in $(get_root_nameservers); do
		if dig @${ns} +dnssec SOA . $@ >/dev/null; then
			return 0
		fi
	done

	# none of the servers was reachable
	return 1
}

enable_dnssec() {
	local status=$(unbound-control get_option val-permissive-mode)

	# Log DNSSEC status
	echo "on" > /var/ipfire/red/dnssec-status

	# Don't do anything if DNSSEC is already activated
	[ "${status}" = "no" ] && return 0

	# Activate DNSSEC and flush cache with any stale and unvalidated data
	unbound-control -q set_option val-permissive-mode: no
	unbound-control -q flush_zone .
}

disable_dnssec() {
	# Log DNSSEC status
	echo "off" > /var/ipfire/red/dnssec-status

	unbound-control -q set_option val-permissive-mode: yes
}

fix_time_if_dns_fail() {
	# If DNS still not work try to init ntp with
	# hardcoded ntp.ipfire.org (81.3.27.46)
	check_red_has_carrier_and_ip
	if [ -e "/var/ipfire/red/iface" -a "${?}" = "1" ]; then
		host 0.ipfire.pool.ntp.org > /dev/null 2>&1
		if [ "${?}" != "0" ]; then
			boot_mesg "DNS still not functioning... Trying to sync time with ntp.ipfire.org (81.3.27.46)..."
			loadproc /usr/local/bin/settime 81.3.27.46
		fi
	fi
}

resolve() {
	local hostname="${1}"

	local found=0
	local ns
	for ns in $(read_name_servers); do
		local answer
		for answer in $(dig +short "@${ns}" A "${hostname}"); do
			found=1

			# Filter out non-IP addresses
			if [[ ! "${answer}" =~ \.$ ]]; then
				echo "${answer}"
			fi
		done

		# End loop when we have got something
		[ ${found} -eq 1 ] && break
	done
}

# Sets up Safe Search for various search engines
write_safe_search_conf() {
	local google_tlds=(
		google.ad
		google.ae
		google.al
		google.am
		google.as
		google.at
		google.az
		google.ba
		google.be
		google.bf
		google.bg
		google.bi
		google.bj
		google.bs
		google.bt
		google.by
		google.ca
		google.cat
		google.cd
		google.cf
		google.cg
		google.ch
		google.ci
		google.cl
		google.cm
		google.cn
		google.co.ao
		google.co.bw
		google.co.ck
		google.co.cr
		google.co.id
		google.co.il
		google.co.in
		google.co.jp
		google.co.ke
		google.co.kr
		google.co.ls
		google.com
		google.co.ma
		google.com.af
		google.com.ag
		google.com.ai
		google.com.ar
		google.com.au
		google.com.bd
		google.com.bh
		google.com.bn
		google.com.bo
		google.com.br
		google.com.bz
		google.com.co
		google.com.cu
		google.com.cy
		google.com.do
		google.com.ec
		google.com.eg
		google.com.et
		google.com.fj
		google.com.gh
		google.com.gi
		google.com.gt
		google.com.hk
		google.com.jm
		google.com.kh
		google.com.kw
		google.com.lb
		google.com.ly
		google.com.mm
		google.com.mt
		google.com.mx
		google.com.my
		google.com.na
		google.com.nf
		google.com.ng
		google.com.ni
		google.com.np
		google.com.om
		google.com.pa
		google.com.pe
		google.com.pg
		google.com.ph
		google.com.pk
		google.com.pr
		google.com.py
		google.com.qa
		google.com.sa
		google.com.sb
		google.com.sg
		google.com.sl
		google.com.sv
		google.com.tj
		google.com.tr
		google.com.tw
		google.com.ua
		google.com.uy
		google.com.vc
		google.com.vn
		google.co.mz
		google.co.nz
		google.co.th
		google.co.tz
		google.co.ug
		google.co.uk
		google.co.uz
		google.co.ve
		google.co.vi
		google.co.za
		google.co.zm
		google.co.zw
		google.cv
		google.cz
		google.de
		google.dj
		google.dk
		google.dm
		google.dz
		google.ee
		google.es
		google.fi
		google.fm
		google.fr
		google.ga
		google.ge
		google.gg
		google.gl
		google.gm
		google.gp
		google.gr
		google.gy
		google.hn
		google.hr
		google.ht
		google.hu
		google.ie
		google.im
		google.iq
		google.is
		google.it
		google.je
		google.jo
		google.kg
		google.ki
		google.kz
		google.la
		google.li
		google.lk
		google.lt
		google.lu
		google.lv
		google.md
		google.me
		google.mg
		google.mk
		google.ml
		google.mn
		google.ms
		google.mu
		google.mv
		google.mw
		google.ne
		google.nl
		google.no
		google.nr
		google.nu
		google.pl
		google.pn
		google.ps
		google.pt
		google.ro
		google.rs
		google.ru
		google.rw
		google.sc
		google.se
		google.sh
		google.si
		google.sk
		google.sm
		google.sn
		google.so
		google.sr
		google.st
		google.td
		google.tg
		google.tk
		google.tl
		google.tm
		google.tn
		google.to
		google.tt
		google.vg
		google.vu
		google.ws
	)

	(
		# Nothing to do if safe search is not enabled
		if [ "${ENABLE_SAFE_SEARCH}" != "on" ]; then
			exit 0
		fi

		# This all belongs into the server: section
		echo "server:"

		# Bing
		echo "	local-zone: bing.com transparent"
		for address in $(resolve "strict.bing.com"); do
			echo "	local-data: \"www.bing.com ${LOCAL_TTL} IN A ${address}\""
		done

		# DuckDuckGo
		echo "	local-zone: duckduckgo.com typetransparent"
		for address in $(resolve "safe.duckduckgo.com"); do
			echo "	local-data: \"duckduckgo.com ${LOCAL_TTL} IN A ${address}\""
		done

		# Google
		addresses="$(resolve "forcesafesearch.google.com")"
		local domain
		for domain in ${google_tlds[@]}; do
			echo "	local-zone: ${domain} transparent"
			for address in ${addresses}; do
				echo "	local-data: \"www.${domain} ${LOCAL_TTL} IN A ${address}\""
			done
		done

		# Yandex
		for domain in yandex.com yandex.ru; do
			echo "	local-zone: ${domain} typetransparent"
			for address in $(resolve "familysearch.${domain}"); do
				echo "	local-data: \"${domain} ${LOCAL_TTL} IN A ${address}\""
			done
		done

		# YouTube
		echo "	local-zone: youtube.com transparent"
		for address in $(resolve "restrictmoderate.youtube.com"); do
			echo "	local-data: \"www.youtube.com ${LOCAL_TTL} IN A ${address}\""
		done
	) > /etc/unbound/safe-search.conf
}

case "$1" in
	start)
		# Print a nicer messagen when unbound is already running
		if pidofproc -s unbound; then
			statusproc /usr/sbin/unbound
			exit 0
		fi

		eval $(/usr/local/bin/readhash /var/ipfire/ethernet/settings)

		# Update configuration files
		write_tuning_conf
		write_forward_conf
		write_safe_search_conf

		boot_mesg "Starting Unbound DNS Proxy..."
		loadproc /usr/sbin/unbound || exit $?

		# Make own hostname resolveable
		own_hostname

		# Update any known forwarding name servers
		update_forwarders

		# Update hosts
		update_hosts

		fix_time_if_dns_fail
		;;

	stop)
		boot_mesg "Stopping Unbound DNS Proxy..."
		killproc /usr/sbin/unbound
		;;

	restart)
		$0 stop
		sleep 1
		$0 start
		;;

	status)
		statusproc /usr/sbin/unbound
		;;

	update-forwarders)
		# Do not try updating forwarders when unbound is not running
		if ! pgrep unbound &>/dev/null; then
			exit 0
		fi

		update_forwarders

		unbound-control flush_negative > /dev/null
		unbound-control flush_bogus > /dev/null

		fix_time_if_dns_fail
		;;

	remove-forwarders)
		# Do not try updating forwarders when unbound is not running
		if ! pgrep unbound &>/dev/null; then
			exit 0
		fi

		remove_forwarders

		unbound-control flush_negative > /dev/null
		unbound-control flush_bogus > /dev/null
		;;


	test-name-server)
		ns=${2}

		test_name_server ${ns}
		ret=${?}

		case "${ret}" in
			0)
				echo "${ns} is validating"
				;;
			2)
				echo "${ns} is DNSSEC-aware"
				;;
			3)
				echo "${ns} is NOT DNSSEC-aware"
				;;
			*)
				echo "Test failed for an unknown reason"
				exit ${ret}
				;;
		esac

		if ns_supports_tcp ${ns}; then
			echo "${ns} supports TCP fallback"
		else
			echo "${ns} does not support TCP fallback"
		fi

		edns_buffer_size=$(ns_determine_edns_buffer_size ${ns})
		if [ -n "${edns_buffer_size}" ]; then
			echo "EDNS buffer size for ${ns}: ${edns_buffer_size}"
		fi

		exit ${ret}
		;;

	resolve)
		resolve "${2}"
		;;

	*)
		echo "Usage: $0 {start|stop|restart|status|update-forwarders|remove-forwarders|test-name-server|resolve}"
		exit 1
		;;
esac

# End $rc_base/init.d/unbound
