local-unbound-setup.sh revision 291767
1#!/bin/sh
2#-
3# Copyright (c) 2013 Dag-Erling Sm��rgrav
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27# $FreeBSD: stable/10/usr.sbin/unbound/local-setup/local-unbound-setup.sh 291767 2015-12-04 13:26:12Z des $
28#
29
30#
31# Configuration variables
32#
33user=""
34unbound_conf=""
35forward_conf=""
36lanzones_conf=""
37control_conf=""
38control_socket=""
39workdir=""
40confdir=""
41chrootdir=""
42anchor=""
43pidfile=""
44resolv_conf=""
45resolvconf_conf=""
46service=""
47start_unbound=""
48forwarders=""
49
50#
51# Global variables
52#
53self=$(basename $(realpath "$0"))
54bkext=$(date "+%Y%m%d.%H%M%S")
55
56#
57# Set default values for unset configuration variables.
58#
59set_defaults() {
60	: ${user:=unbound}
61	: ${workdir:=/var/unbound}
62	: ${confdir:=${workdir}/conf.d}
63	: ${unbound_conf:=${workdir}/unbound.conf}
64	: ${forward_conf:=${workdir}/forward.conf}
65	: ${lanzones_conf:=${workdir}/lan-zones.conf}
66	: ${control_conf:=${workdir}/control.conf}
67	: ${control_socket:=/var/run/local_unbound.ctl}
68	: ${anchor:=${workdir}/root.key}
69	: ${pidfile:=/var/run/local_unbound.pid}
70	: ${resolv_conf:=/etc/resolv.conf}
71	: ${resolvconf_conf:=/etc/resolvconf.conf}
72	: ${service:=local_unbound}
73	: ${start_unbound:=yes}
74}
75
76#
77# Verify that the configuration files are inside the working
78# directory, and if so, set the chroot directory accordingly.
79#
80set_chrootdir() {
81	chrootdir="${workdir}"
82	for file in "${unbound_conf}" "${forward_conf}" \
83	    "${lanzones_conf}" "${control_conf}" "${anchor}" ; do
84		if [ "${file#${workdir%/}/}" = "${file}" ] ; then
85			echo "warning: ${file} is outside ${workdir}" >&2
86			chrootdir=""
87		fi
88	done
89	if [ -z "${chrootdir}" ] ; then
90		echo "warning: disabling chroot" >&2
91	fi
92}
93
94#
95# Scan through /etc/resolv.conf looking for uncommented nameserver
96# lines that don't point to localhost and return their values.
97#
98get_nameservers() {
99	while read line ; do
100		local bareline=${line%%\#*}
101		local key=${bareline%% *}
102		local value=${bareline#* }
103		case ${key} in
104		nameserver)
105			case ${value} in
106			127.0.0.1|::1|localhost|localhost.*)
107				;;
108			*)
109				echo "${value}"
110				;;
111			esac
112			;;
113		esac
114	done
115}
116
117#
118# Scan through /etc/resolv.conf looking for uncommented nameserver
119# lines.  Comment out any that don't point to localhost.  Finally,
120# append a nameserver line that points to localhost, if there wasn't
121# one already, and enable the edns0 option.
122#
123gen_resolv_conf() {
124	local localhost=no
125	local edns0=no
126	while read line ; do
127		local bareline=${line%%\#*}
128		local key=${bareline%% *}
129		local value=${bareline#* }
130		case ${key} in
131		nameserver)
132			case ${value} in
133			127.0.0.1|::1|localhost|localhost.*)
134				localhost=yes
135				;;
136			*)
137				echo -n "# "
138				;;
139			esac
140			;;
141		options)
142			case ${value} in
143			*edns0*)
144				edns0=yes
145				;;
146			esac
147			;;
148		esac
149		echo "${line}"
150	done
151	if [ "${localhost}" = "no" ] ; then
152		echo "nameserver 127.0.0.1"
153	fi
154	if [ "${edns0}" = "no" ] ; then
155		echo "options edns0"
156	fi
157}
158
159#
160# Boilerplate
161#
162do_not_edit() {
163	echo "# This file was generated by $self."
164	echo "# Modifications will be overwritten."
165}
166
167#
168# Generate resolvconf.conf so it updates forward.conf in addition to
169# resolv.conf.  Note "in addition to" rather than "instead of",
170# because we still want it to update the domain name and search path
171# if they change.  Setting name_servers to "127.0.0.1" ensures that
172# the libc resolver will try unbound first.
173#
174gen_resolvconf_conf() {
175	local style="$1"
176	do_not_edit
177	echo "resolv_conf=\"/dev/null\" # prevent updating ${resolv_conf}"
178	if [ "${style}" = "dynamic" ] ; then
179		echo "unbound_conf=\"${forward_conf}\""
180		echo "unbound_pid=\"${pidfile}\""
181		echo "unbound_service=\"${service}\""
182		# resolvconf(8) likes to restart rather than reload
183		echo "unbound_restart=\"service ${service} reload\""
184	else
185		echo "# Static DNS configuration"
186	fi
187}
188
189#
190# Generate forward.conf
191#
192gen_forward_conf() {
193	do_not_edit
194	echo "forward-zone:"
195	echo "        name: ."
196	for forwarder ; do
197		if expr "${forwarder}" : "^[0-9A-Fa-f:.]\{1,\}$" >/dev/null ; then
198			echo "        forward-addr: ${forwarder}"
199		else
200			echo "        forward-host: ${forwarder}"
201		fi
202	done
203}
204
205#
206# Generate lan-zones.conf
207#
208gen_lanzones_conf() {
209	do_not_edit
210	echo "server:"
211	echo "        # Unblock reverse lookups for LAN addresses"
212	echo "        unblock-lan-zones: yes"
213	echo "        domain-insecure: 10.in-addr.arpa."
214	echo "        domain-insecure: 127.in-addr.arpa."
215	echo "        domain-insecure: 16.172.in-addr.arpa."
216	echo "        domain-insecure: 17.172.in-addr.arpa."
217	echo "        domain-insecure: 18.172.in-addr.arpa."
218	echo "        domain-insecure: 19.172.in-addr.arpa."
219	echo "        domain-insecure: 20.172.in-addr.arpa."
220	echo "        domain-insecure: 21.172.in-addr.arpa."
221	echo "        domain-insecure: 22.172.in-addr.arpa."
222	echo "        domain-insecure: 23.172.in-addr.arpa."
223	echo "        domain-insecure: 24.172.in-addr.arpa."
224	echo "        domain-insecure: 25.172.in-addr.arpa."
225	echo "        domain-insecure: 26.172.in-addr.arpa."
226	echo "        domain-insecure: 27.172.in-addr.arpa."
227	echo "        domain-insecure: 28.172.in-addr.arpa."
228	echo "        domain-insecure: 29.172.in-addr.arpa."
229	echo "        domain-insecure: 30.172.in-addr.arpa."
230	echo "        domain-insecure: 31.172.in-addr.arpa."
231	echo "        domain-insecure: 168.192.in-addr.arpa."
232	echo "        domain-insecure: 254.169.in-addr.arpa."
233	echo "        domain-insecure: d.f.ip6.arpa."
234	echo "        domain-insecure: 8.e.ip6.arpa."
235	echo "        domain-insecure: 9.e.ip6.arpa."
236	echo "        domain-insecure: a.e.ip6.arpa."
237	echo "        domain-insecure: b.e.ip6.arpa."
238}
239
240#
241# Generate control.conf
242#
243gen_control_conf() {
244	do_not_edit
245	echo "remote-control:"
246	echo "        control-enable: yes"
247	echo "        control-interface: ${control_socket}"
248	echo "        control-use-cert: no"
249}
250
251#
252# Generate unbound.conf
253#
254gen_unbound_conf() {
255	do_not_edit
256	echo "server:"
257	echo "        username: ${user}"
258	echo "        directory: ${workdir}"
259	echo "        chroot: ${chrootdir}"
260	echo "        pidfile: ${pidfile}"
261	echo "        auto-trust-anchor-file: ${anchor}"
262	echo ""
263	if [ -f "${forward_conf}" ] ; then
264		echo "include: ${forward_conf}"
265	fi
266	if [ -f "${lanzones_conf}" ] ; then
267		echo "include: ${lanzones_conf}"
268	fi
269	if [ -f "${control_conf}" ] ; then
270		echo "include: ${control_conf}"
271	fi
272	if [ -d "${confdir}" ] ; then
273		echo "include: ${confdir}/*.conf"
274	fi
275}
276
277#
278# Replace one file with another, making a backup copy of the first,
279# but only if the new file is different from the old.
280#
281replace() {
282	local file="$1"
283	local newfile="$2"
284	if [ ! -f "${file}" ] ; then
285		echo "${file} created"
286		mv "${newfile}" "${file}"
287	elif ! cmp -s "${file}" "${newfile}" ; then
288		local oldfile="${file}.${bkext}"
289		echo "original ${file} saved as ${oldfile}"
290		mv "${file}" "${oldfile}"
291		mv "${newfile}" "${file}"
292	else
293		echo "${file} not modified"
294		rm "${newfile}"
295	fi
296}
297
298#
299# Print usage message and exit
300#
301usage() {
302	exec >&2
303	echo "usage: $self [options] [forwarder ...]"
304	echo "options:"
305	echo "    -n          do not start unbound"
306	echo "    -a path     full path to trust anchor file"
307	echo "    -C path     full path to additional configuration directory"
308	echo "    -c path     full path to unbound configuration file"
309	echo "    -f path     full path to forwarding configuration"
310	echo "    -O path     full path to remote control socket"
311	echo "    -o path     full path to remote control configuration"
312	echo "    -p path     full path to pid file"
313	echo "    -R path     full path to resolvconf.conf"
314	echo "    -r path     full path to resolv.conf"
315	echo "    -s service  name of unbound service"
316	echo "    -u user     user to run unbound as"
317	echo "    -w path     full path to working directory"
318	exit 1
319}
320
321#
322# Main
323#
324main() {
325	umask 022
326
327	#
328	# Parse and validate command-line options
329	#
330	while getopts "a:C:c:f:no:p:R:r:s:u:w:" option ; do
331		case $option in
332		a)
333			anchor="$OPTARG"
334			;;
335		C)
336			confdir="$OPTARG"
337			;;
338		c)
339			unbound_conf="$OPTARG"
340			;;
341		f)
342			forward_conf="$OPTARG"
343			;;
344		n)
345			start_unbound="no"
346			;;
347		O)
348			control_socket="$OPTARG"
349			;;
350		o)
351			control_conf="$OPTARG"
352			;;	
353		p)
354			pidfile="$OPTARG"
355			;;
356		R)
357			resolvconf_conf="$OPTARG"
358			;;
359		r)
360			resolv_conf="$OPTARG"
361			;;
362		s)
363			service="$OPTARG"
364			;;
365		u)
366			user="$OPTARG"
367			;;
368		w)
369			workdir="$OPTARG"
370			;;
371		*)
372			usage
373			;;
374		esac
375	done
376	shift $((OPTIND-1))
377	set_defaults
378
379	#
380	# Get the list of forwarders, either from the command line or
381	# from resolv.conf.
382	#
383	forwarders="$@"
384	if [ -z "$forwarders" ] ; then
385		echo "Extracting forwarders from ${resolv_conf}."
386		forwarders=$(get_nameservers <"${resolv_conf}")
387		style=dynamic
388	else
389		style=static
390	fi
391
392	#
393	# Generate forward.conf.
394	#
395	if [ -z "${forwarders}" ] ; then
396		echo -n "No forwarders found in ${resolv_conf##*/}, "
397		if [ -f "${forward_conf}" ] ; then
398			echo "using existing ${forward_conf##*/}."
399		else
400			echo "unbound will recurse."
401		fi
402	else
403		local tmp_forward_conf=$(mktemp -u "${forward_conf}.XXXXX")
404		gen_forward_conf ${forwarders} | unexpand >"${tmp_forward_conf}"
405		replace "${forward_conf}" "${tmp_forward_conf}"
406	fi
407
408	#
409	# Generate lan-zones.conf.
410	#
411	local tmp_lanzones_conf=$(mktemp -u "${lanzones_conf}.XXXXX")
412	gen_lanzones_conf | unexpand >"${tmp_lanzones_conf}"
413	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
414
415	#
416	# Generate control.conf.
417	#
418	local tmp_control_conf=$(mktemp -u "${control_conf}.XXXXX")
419	gen_control_conf | unexpand >"${tmp_control_conf}"
420	replace "${control_conf}" "${tmp_control_conf}"
421
422	#
423	# Generate unbound.conf.
424	#
425	local tmp_unbound_conf=$(mktemp -u "${unbound_conf}.XXXXX")
426	set_chrootdir
427	gen_unbound_conf | unexpand >"${tmp_unbound_conf}"
428	replace "${unbound_conf}" "${tmp_unbound_conf}"
429
430	#
431	# Start unbound, unless requested not to.  Stop immediately if
432	# it is not enabled so we don't end up with a resolv.conf that
433	# points into nothingness.  We could "onestart" it, but it
434	# wouldn't stick.
435	#
436	if [ "${start_unbound}" = "no" ] ; then
437		# skip
438	elif ! service "${service}" enabled ; then
439		echo "Please enable $service in rc.conf(5) and try again."
440		return 1
441	elif ! service "${service}" restart ; then
442		echo "Failed to start $service."
443		return 1
444	fi
445
446	#
447	# Rewrite resolvconf.conf so resolvconf updates forward.conf
448	# instead of resolv.conf.
449	#
450	local tmp_resolvconf_conf=$(mktemp -u "${resolvconf_conf}.XXXXX")
451	gen_resolvconf_conf "${style}" | unexpand >"${tmp_resolvconf_conf}"
452	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
453
454	#
455	# Finally, rewrite resolv.conf.
456	#
457	local tmp_resolv_conf=$(mktemp -u "${resolv_conf}.XXXXX")
458	gen_resolv_conf <"${resolv_conf}" | unexpand >"${tmp_resolv_conf}"
459	replace "${resolv_conf}" "${tmp_resolv_conf}"
460}
461
462main "$@"
463