1255809Sdes#!/bin/sh
2255809Sdes#-
3255809Sdes# Copyright (c) 2013 Dag-Erling Sm��rgrav
4255809Sdes# All rights reserved.
5255809Sdes#
6255809Sdes# Redistribution and use in source and binary forms, with or without
7255809Sdes# modification, are permitted provided that the following conditions
8255809Sdes# are met:
9255809Sdes# 1. Redistributions of source code must retain the above copyright
10255809Sdes#    notice, this list of conditions and the following disclaimer.
11255809Sdes# 2. Redistributions in binary form must reproduce the above copyright
12255809Sdes#    notice, this list of conditions and the following disclaimer in the
13255809Sdes#    documentation and/or other materials provided with the distribution.
14255809Sdes#
15255809Sdes# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16255809Sdes# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17255809Sdes# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18255809Sdes# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19255809Sdes# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20255809Sdes# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21255809Sdes# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22255809Sdes# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23255809Sdes# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24255809Sdes# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25255809Sdes# SUCH DAMAGE.
26255809Sdes#
27255809Sdes# $FreeBSD$
28255809Sdes#
29255809Sdes
30255809Sdes#
31255809Sdes# Configuration variables
32255809Sdes#
33255809Sdesuser=""
34255809Sdesunbound_conf=""
35255809Sdesforward_conf=""
36269257Sdeslanzones_conf=""
37255809Sdesworkdir=""
38269257Sdesconfdir=""
39255809Sdeschrootdir=""
40255809Sdesanchor=""
41255809Sdespidfile=""
42255809Sdesresolv_conf=""
43255809Sdesresolvconf_conf=""
44255809Sdesservice=""
45255809Sdesstart_unbound=""
46255809Sdesforwarders=""
47255809Sdes
48255809Sdes#
49255809Sdes# Global variables
50255809Sdes#
51255809Sdesself=$(basename $(realpath "$0"))
52255809Sdesbkext=$(date "+%Y%m%d.%H%M%S")
53255809Sdes
54255809Sdes#
55255809Sdes# Set default values for unset configuration variables.
56255809Sdes#
57255809Sdesset_defaults() {
58255809Sdes	: ${user:=unbound}
59255809Sdes	: ${workdir:=/var/unbound}
60269257Sdes	: ${confdir:=${workdir}/conf.d}
61255809Sdes	: ${unbound_conf:=${workdir}/unbound.conf}
62255809Sdes	: ${forward_conf:=${workdir}/forward.conf}
63269257Sdes	: ${lanzones_conf:=${workdir}/lan-zones.conf}
64255809Sdes	: ${anchor:=${workdir}/root.key}
65255809Sdes	: ${pidfile:=/var/run/local_unbound.pid}
66255809Sdes	: ${resolv_conf:=/etc/resolv.conf}
67255809Sdes	: ${resolvconf_conf:=/etc/resolvconf.conf}
68255809Sdes	: ${service:=local_unbound}
69255809Sdes	: ${start_unbound:=yes}
70255809Sdes}
71255809Sdes
72255809Sdes#
73255809Sdes# Verify that the configuration files are inside the working
74255809Sdes# directory, and if so, set the chroot directory accordingly.
75255809Sdes#
76255809Sdesset_chrootdir() {
77255809Sdes	chrootdir="${workdir}"
78269257Sdes	for file in "${unbound_conf}" "${forward_conf}" \
79269257Sdes	    "${lanzones_conf}" "${anchor}" ; do
80255809Sdes		if [ "${file#${workdir%/}/}" = "${file}" ] ; then
81255809Sdes			echo "warning: ${file} is outside ${workdir}" >&2
82255809Sdes			chrootdir=""
83255809Sdes		fi
84255809Sdes	done
85255809Sdes	if [ -z "${chrootdir}" ] ; then
86255809Sdes		echo "warning: disabling chroot" >&2
87255809Sdes	fi
88255809Sdes}
89255809Sdes
90255809Sdes#
91255809Sdes# Scan through /etc/resolv.conf looking for uncommented nameserver
92255809Sdes# lines that don't point to localhost and return their values.
93255809Sdes#
94255809Sdesget_nameservers() {
95255809Sdes	while read line ; do
96255809Sdes		local bareline=${line%%\#*}
97255809Sdes		local key=${bareline%% *}
98255809Sdes		local value=${bareline#* }
99255809Sdes		case ${key} in
100255809Sdes		nameserver)
101255809Sdes			case ${value} in
102255809Sdes			127.0.0.1|::1|localhost|localhost.*)
103255809Sdes				;;
104255809Sdes			*)
105255809Sdes				echo "${value}"
106255809Sdes				;;
107255809Sdes			esac
108255809Sdes			;;
109255809Sdes		esac
110255809Sdes	done
111255809Sdes}
112255809Sdes
113255809Sdes#
114255809Sdes# Scan through /etc/resolv.conf looking for uncommented nameserver
115255809Sdes# lines.  Comment out any that don't point to localhost.  Finally,
116255809Sdes# append a nameserver line that points to localhost, if there wasn't
117255809Sdes# one already, and enable the edns0 option.
118255809Sdes#
119255809Sdesgen_resolv_conf() {
120255809Sdes	local localhost=no
121255809Sdes	local edns0=no
122255809Sdes	while read line ; do
123255809Sdes		local bareline=${line%%\#*}
124255809Sdes		local key=${bareline%% *}
125255809Sdes		local value=${bareline#* }
126255809Sdes		case ${key} in
127255809Sdes		nameserver)
128255809Sdes			case ${value} in
129255809Sdes			127.0.0.1|::1|localhost|localhost.*)
130255809Sdes				localhost=yes
131255809Sdes				;;
132255809Sdes			*)
133255809Sdes				echo -n "# "
134255809Sdes				;;
135255809Sdes			esac
136255809Sdes			;;
137255809Sdes		options)
138255809Sdes			case ${value} in
139255809Sdes			*edns0*)
140255809Sdes				edns0=yes
141255809Sdes				;;
142255809Sdes			esac
143255809Sdes			;;
144255809Sdes		esac
145255809Sdes		echo "${line}"
146255809Sdes	done
147255809Sdes	if [ "${localhost}" = "no" ] ; then
148255809Sdes		echo "nameserver 127.0.0.1"
149255809Sdes	fi
150255809Sdes	if [ "${edns0}" = "no" ] ; then
151255809Sdes		echo "options edns0"
152255809Sdes	fi
153255809Sdes}
154255809Sdes
155255809Sdes#
156255809Sdes# Generate resolvconf.conf so it updates forward.conf in addition to
157255809Sdes# resolv.conf.  Note "in addition to" rather than "instead of",
158255809Sdes# because we still want it to update the domain name and search path
159255809Sdes# if they change.  Setting name_servers to "127.0.0.1" ensures that
160255809Sdes# the libc resolver will try unbound first.
161255809Sdes#
162255809Sdesgen_resolvconf_conf() {
163255809Sdes	echo "# Generated by $self"
164255826Sdes	echo "resolv_conf=\"/dev/null\" # prevent updating ${resolv_conf}"
165255809Sdes	echo "unbound_conf=\"${forward_conf}\""
166255809Sdes	echo "unbound_pid=\"${pidfile}\""
167255809Sdes	echo "unbound_service=\"${service}\""
168255826Sdes	# resolvconf(8) likes to restart rather than reload
169255826Sdes	echo "unbound_restart=\"service ${service} reload\""
170255809Sdes}
171255809Sdes
172255809Sdes#
173255809Sdes# Generate forward.conf
174255809Sdes#
175255809Sdesgen_forward_conf() {
176255809Sdes	echo "# Generated by $self"
177269257Sdes	echo "# Do not edit this file."
178255809Sdes	echo "forward-zone:"
179255809Sdes	echo "        name: ."
180255809Sdes	for forwarder ; do
181271760Sdes		if expr "${forwarder}" : "^[0-9A-Fa-f:.]\{1,\}$" >/dev/null ; then
182255809Sdes			echo "        forward-addr: ${forwarder}"
183255809Sdes		else
184255809Sdes			echo "        forward-host: ${forwarder}"
185255809Sdes		fi
186255809Sdes	done
187255809Sdes}
188255809Sdes
189255809Sdes#
190269257Sdes# Generate lan-zones.conf
191269257Sdes#
192269257Sdesgen_lanzones_conf() {
193269257Sdes	echo "# Generated by $self"
194269257Sdes	echo "# Do not edit this file."
195269257Sdes	echo "server:"
196269257Sdes	echo "        # Unblock reverse lookups for LAN addresses"
197269257Sdes	echo "        unblock-lan-zones: yes"
198269257Sdes	echo "        domain-insecure: 10.in-addr.arpa."
199269257Sdes	echo "        domain-insecure: 127.in-addr.arpa."
200269257Sdes	echo "        domain-insecure: 16.172.in-addr.arpa."
201269257Sdes	echo "        domain-insecure: 17.172.in-addr.arpa."
202269257Sdes	echo "        domain-insecure: 18.172.in-addr.arpa."
203269257Sdes	echo "        domain-insecure: 19.172.in-addr.arpa."
204269257Sdes	echo "        domain-insecure: 20.172.in-addr.arpa."
205269257Sdes	echo "        domain-insecure: 21.172.in-addr.arpa."
206269257Sdes	echo "        domain-insecure: 22.172.in-addr.arpa."
207269257Sdes	echo "        domain-insecure: 23.172.in-addr.arpa."
208269257Sdes	echo "        domain-insecure: 24.172.in-addr.arpa."
209269257Sdes	echo "        domain-insecure: 25.172.in-addr.arpa."
210269257Sdes	echo "        domain-insecure: 26.172.in-addr.arpa."
211269257Sdes	echo "        domain-insecure: 27.172.in-addr.arpa."
212269257Sdes	echo "        domain-insecure: 28.172.in-addr.arpa."
213269257Sdes	echo "        domain-insecure: 29.172.in-addr.arpa."
214269257Sdes	echo "        domain-insecure: 30.172.in-addr.arpa."
215269257Sdes	echo "        domain-insecure: 31.172.in-addr.arpa."
216269257Sdes	echo "        domain-insecure: 168.192.in-addr.arpa."
217269257Sdes	echo "        domain-insecure: 254.169.in-addr.arpa."
218269257Sdes	echo "        domain-insecure: d.f.ip6.arpa."
219269257Sdes	echo "        domain-insecure: 8.e.ip6.arpa."
220269257Sdes	echo "        domain-insecure: 9.e.ip6.arpa."
221269257Sdes	echo "        domain-insecure: a.e.ip6.arpa."
222269257Sdes	echo "        domain-insecure: b.e.ip6.arpa."
223269257Sdes}
224269257Sdes
225269257Sdes#
226255809Sdes# Generate unbound.conf
227255809Sdes#
228255809Sdesgen_unbound_conf() {
229255809Sdes	echo "# Generated by $self"
230255809Sdes	echo "server:"
231255809Sdes	echo "        username: ${user}"
232255809Sdes	echo "        directory: ${workdir}"
233255809Sdes	echo "        chroot: ${chrootdir}"
234255809Sdes	echo "        pidfile: ${pidfile}"
235255809Sdes	echo "        auto-trust-anchor-file: ${anchor}"
236255809Sdes	echo ""
237255809Sdes	if [ -f "${forward_conf}" ] ; then
238255809Sdes		echo "include: ${forward_conf}"
239255809Sdes	fi
240269257Sdes	if [ -f "${lanzones_conf}" ] ; then
241269257Sdes		echo "include: ${lanzones_conf}"
242269257Sdes	fi
243269257Sdes	if [ -d "${confdir}" ] ; then
244269257Sdes		echo "include: ${confdir}/*.conf"
245269257Sdes	fi
246255809Sdes}
247255809Sdes
248255809Sdes#
249255809Sdes# Replace one file with another, making a backup copy of the first,
250255809Sdes# but only if the new file is different from the old.
251255809Sdes#
252255809Sdesreplace() {
253255809Sdes	local file="$1"
254255809Sdes	local newfile="$2"
255255809Sdes	if [ ! -f "${file}" ] ; then
256255809Sdes		echo "${file} created"
257255809Sdes		mv "${newfile}" "${file}"
258255809Sdes	elif ! cmp -s "${file}" "${newfile}" ; then
259255809Sdes		local oldfile="${file}.${bkext}"
260255809Sdes		echo "original ${file} saved as ${oldfile}"
261255809Sdes		mv "${file}" "${oldfile}"
262255809Sdes		mv "${newfile}" "${file}"
263255809Sdes	else
264255809Sdes		echo "${file} not modified"
265255809Sdes		rm "${newfile}"
266255809Sdes	fi
267255809Sdes}
268255809Sdes
269255809Sdes#
270255809Sdes# Print usage message and exit
271255809Sdes#
272255809Sdesusage() {
273255809Sdes	exec >&2
274255809Sdes	echo "usage: $self [options] [forwarder ...]"
275255809Sdes	echo "options:"
276255809Sdes	echo "    -n          do not start unbound"
277255809Sdes	echo "    -a path     full path to trust anchor file"
278269257Sdes	echo "    -C path     full path to additional configuration directory"
279269257Sdes	echo "    -c path     full path to unbound configuration file"
280255809Sdes	echo "    -f path     full path to forwarding configuration"
281255809Sdes	echo "    -p path     full path to pid file"
282255809Sdes	echo "    -R path     full path to resolvconf.conf"
283255809Sdes	echo "    -r path     full path to resolv.conf"
284255809Sdes	echo "    -s service  name of unbound service"
285255809Sdes	echo "    -u user     user to run unbound as"
286255809Sdes	echo "    -w path     full path to working directory"
287255809Sdes	exit 1
288255809Sdes}
289255809Sdes
290255809Sdes#
291255809Sdes# Main
292255809Sdes#
293255809Sdesmain() {
294255809Sdes	umask 022
295255809Sdes
296255809Sdes	#
297255809Sdes	# Parse and validate command-line options
298255809Sdes	#
299269257Sdes	while getopts "a:C:c:f:np:R:r:s:u:w:" option ; do
300255809Sdes		case $option in
301255809Sdes		a)
302255809Sdes			anchor="$OPTARG"
303255809Sdes			;;
304269257Sdes		C)
305269257Sdes			confdir="$OPTARG"
306269257Sdes			;;
307255809Sdes		c)
308255809Sdes			unbound_conf="$OPTARG"
309255809Sdes			;;
310255809Sdes		f)
311255809Sdes			forward_conf="$OPTARG"
312255809Sdes			;;
313255809Sdes		n)
314255809Sdes			start_unbound="no"
315255809Sdes			;;
316255809Sdes		p)
317255809Sdes			pidfile="$OPTARG"
318255809Sdes			;;
319255809Sdes		R)
320255809Sdes			resolvconf_conf="$OPTARG"
321255809Sdes			;;
322255809Sdes		r)
323255809Sdes			resolv_conf="$OPTARG"
324255809Sdes			;;
325255809Sdes		s)
326255809Sdes			service="$OPTARG"
327255809Sdes			;;
328255809Sdes		u)
329255809Sdes			user="$OPTARG"
330255809Sdes			;;
331255809Sdes		w)
332255809Sdes			workdir="$OPTARG"
333255809Sdes			;;
334255809Sdes		*)
335255809Sdes			usage
336255809Sdes			;;
337255809Sdes		esac
338255809Sdes	done
339255809Sdes	shift $((OPTIND-1))
340255809Sdes	set_defaults
341255809Sdes
342255809Sdes	#
343255809Sdes	# Get the list of forwarders, either from the command line or
344255809Sdes	# from resolv.conf.
345255809Sdes	#
346255809Sdes	forwarders="$@"
347255809Sdes	if [ -z "$forwarders" ] ; then
348255809Sdes		echo "Extracting forwarders from ${resolv_conf}."
349255809Sdes		forwarders=$(get_nameservers <"${resolv_conf}")
350255809Sdes	fi
351255809Sdes
352255809Sdes	#
353255809Sdes	# Generate forward.conf.
354255809Sdes	#
355255809Sdes	if [ -z "${forwarders}" ] ; then
356255809Sdes		echo -n "No forwarders found in ${resolv_conf##*/}, "
357255809Sdes		if [ -f "${forward_conf}" ] ; then
358255809Sdes			echo "using existing ${forward_conf##*/}."
359255809Sdes		else
360255809Sdes			echo "unbound will recurse."
361255809Sdes		fi
362255809Sdes	else
363255809Sdes		local tmp_forward_conf=$(mktemp -u "${forward_conf}.XXXXX")
364255809Sdes		gen_forward_conf ${forwarders} >"${tmp_forward_conf}"
365255809Sdes		replace "${forward_conf}" "${tmp_forward_conf}"
366255809Sdes	fi
367255809Sdes
368255809Sdes	#
369269257Sdes	# Generate lan-zones.conf.
370269257Sdes	#
371269257Sdes	local tmp_lanzones_conf=$(mktemp -u "${lanzones_conf}.XXXXX")
372269257Sdes	gen_lanzones_conf >"${tmp_lanzones_conf}"
373269257Sdes	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
374269257Sdes
375269257Sdes	#
376255809Sdes	# Generate unbound.conf.
377255809Sdes	#
378255809Sdes	local tmp_unbound_conf=$(mktemp -u "${unbound_conf}.XXXXX")
379255809Sdes	set_chrootdir
380255809Sdes	gen_unbound_conf >"${tmp_unbound_conf}"
381255809Sdes	replace "${unbound_conf}" "${tmp_unbound_conf}"
382255809Sdes
383255809Sdes	#
384255809Sdes	# Start unbound, unless requested not to.  Stop immediately if
385255809Sdes	# it is not enabled so we don't end up with a resolv.conf that
386255809Sdes	# points into nothingness.  We could "onestart" it, but it
387255809Sdes	# wouldn't stick.
388255809Sdes	#
389255809Sdes	if [ "${start_unbound}" = "no" ] ; then
390255809Sdes		# skip
391255809Sdes	elif ! service "${service}" enabled ; then
392255809Sdes		echo "Please enable $service in rc.conf(5) and try again."
393255809Sdes		return 1
394255809Sdes	elif ! service "${service}" restart ; then
395255809Sdes		echo "Failed to start $service."
396255809Sdes		return 1
397255809Sdes	fi
398255809Sdes
399255809Sdes	#
400255809Sdes	# Rewrite resolvconf.conf so resolvconf updates forward.conf
401255809Sdes	# instead of resolv.conf.
402255809Sdes	#
403255809Sdes	local tmp_resolvconf_conf=$(mktemp -u "${resolvconf_conf}.XXXXX")
404255809Sdes	gen_resolvconf_conf >"${tmp_resolvconf_conf}"
405255809Sdes	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
406255809Sdes
407255809Sdes	#
408255809Sdes	# Finally, rewrite resolv.conf.
409255809Sdes	#
410255809Sdes	local tmp_resolv_conf=$(mktemp -u "${resolv_conf}.XXXXX")
411255809Sdes	gen_resolv_conf <"${resolv_conf}" >"${tmp_resolv_conf}"
412255809Sdes	replace "${resolv_conf}" "${tmp_resolv_conf}"
413255809Sdes}
414255809Sdes
415255809Sdesmain "$@"
416