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