xref: /freebsd/usr.sbin/unbound/setup/local-unbound-setup.sh (revision de3faa85d8f99d260cbfa6242dd8e4ece693e4f8)
1#!/bin/sh
2#-
3# SPDX-License-Identifier: BSD-2-Clause
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#
30
31D="${DESTDIR}"
32echo "destination: ${D}"
33
34#
35# Configuration variables
36#
37user=""
38unbound_conf=""
39forward_conf=""
40lanzones_conf=""
41control_conf=""
42control_socket=""
43workdir=""
44confdir=""
45chrootdir=""
46anchor=""
47pidfile=""
48resolv_conf=""
49resolvconf_conf=""
50service=""
51start_unbound=""
52use_tls=""
53forwarders=""
54
55#
56# Global variables
57#
58self=$(basename $(realpath "$0"))
59bkdir=/var/backups
60bkext=$(date "+%Y%m%d.%H%M%S")
61
62#
63# Regular expressions
64#
65RE_octet="([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"
66RE_ipv4="(${RE_octet}(\\.${RE_octet}){3})"
67RE_word="([0-9A-Fa-f]{1,4})"
68RE_ipv6="((${RE_word}:){1,}(:|${RE_word}?(:${RE_word})*)|::1)"
69RE_port="([1-9][0-9]{0,3}|[1-5][0-9]{4,4}|6([0-4][0-9]{3}|5([0-4][0-9]{2}|5([0-2][0-9]|3[0-5]))))"
70RE_dnsname="([0-9A-Za-z-]{1,}(\\.[0-9A-Za-z-]{1,})*\\.?)"
71RE_forward_addr="((${RE_ipv4}|${RE_ipv6})(@${RE_port})?)"
72RE_forward_name="(${RE_dnsname}(@${RE_port})?)"
73RE_forward_tls="(${RE_forward_addr}(#${RE_dnsname})?)"
74
75#
76# Set default values for unset configuration variables.
77#
78set_defaults() {
79	: ${user:=unbound}
80	: ${workdir:=/var/unbound}
81	: ${confdir:=${workdir}/conf.d}
82	: ${unbound_conf:=${workdir}/unbound.conf}
83	: ${forward_conf:=${workdir}/forward.conf}
84	: ${lanzones_conf:=${workdir}/lan-zones.conf}
85	: ${control_conf:=${workdir}/control.conf}
86	: ${control_socket:=/var/run/local_unbound.ctl}
87	: ${anchor:=${workdir}/root.key}
88	: ${pidfile:=/var/run/local_unbound.pid}
89	: ${resolv_conf:=/etc/resolv.conf}
90	: ${resolvconf_conf:=/etc/resolvconf.conf}
91	: ${service:=local_unbound}
92	: ${start_unbound:=yes}
93	: ${use_tls:=no}
94}
95
96#
97# Verify that the configuration files are inside the working
98# directory, and if so, set the chroot directory accordingly.
99#
100set_chrootdir() {
101	chrootdir="${workdir}"
102	for file in "${unbound_conf}" "${forward_conf}" \
103	    "${lanzones_conf}" "${control_conf}" "${anchor}" ; do
104		if [ "${file#${workdir%/}/}" = "${file}" ] ; then
105			echo "warning: ${file} is outside ${workdir}" >&2
106			chrootdir=""
107		fi
108	done
109	if [ -z "${chrootdir}" ] ; then
110		echo "warning: disabling chroot" >&2
111	fi
112}
113
114#
115# Scan through /etc/resolv.conf looking for uncommented nameserver
116# lines that don't point to localhost and return their values.
117#
118get_nameservers() {
119	while read line ; do
120		local bareline=${line%%\#*}
121		local key=${bareline%% *}
122		local value=${bareline#* }
123		case ${key} in
124		nameserver)
125			case ${value} in
126			127.0.0.1|::1|localhost|localhost.*)
127				;;
128			*)
129				echo "${value}"
130				;;
131			esac
132			;;
133		esac
134	done
135}
136
137#
138# Scan through /etc/resolv.conf looking for uncommented nameserver
139# lines.  Comment out any that don't point to localhost.  Finally,
140# append a nameserver line that points to localhost, if there wasn't
141# one already, and enable the edns0 option.
142#
143gen_resolv_conf() {
144	local localhost=no
145	local edns0=no
146	while read line ; do
147		local bareline=${line%%\#*}
148		local key=${bareline%% *}
149		local value=${bareline#* }
150		case ${key} in
151		nameserver)
152			case ${value} in
153			127.0.0.1|::1|localhost|localhost.*)
154				localhost=yes
155				;;
156			*)
157				echo -n "# "
158				;;
159			esac
160			;;
161		options)
162			case ${value} in
163			*edns0*)
164				edns0=yes
165				;;
166			esac
167			;;
168		esac
169		echo "${line}"
170	done
171	if [ "${localhost}" = "no" ] ; then
172		echo "nameserver 127.0.0.1"
173	fi
174	if [ "${edns0}" = "no" ] ; then
175		echo "options edns0"
176	fi
177}
178
179#
180# Boilerplate
181#
182do_not_edit() {
183	echo "# This file was generated by $self."
184	echo "# Modifications will be overwritten."
185}
186
187#
188# Generate resolvconf.conf so it updates forward.conf in addition to
189# resolv.conf.  Note "in addition to" rather than "instead of",
190# because we still want it to update the domain name and search path
191# if they change.  Setting name_servers to "127.0.0.1" ensures that
192# the libc resolver will try unbound first.
193#
194gen_resolvconf_conf() {
195	local style="$1"
196	do_not_edit
197	echo "libc=\"NO\""
198	if [ "${style}" = "dynamic" ] ; then
199		echo "unbound_conf=\"${forward_conf}\""
200		echo "unbound_pid=\"${pidfile}\""
201		echo "unbound_service=\"${service}\""
202		# resolvconf(8) likes to restart rather than reload
203		echo "unbound_restart=\"service ${service} reload\""
204	else
205		echo "# Static DNS configuration"
206	fi
207}
208
209#
210# Generate forward.conf
211#
212gen_forward_conf() {
213	do_not_edit
214	echo "forward-zone:"
215	echo "        name: ."
216	for forwarder ; do echo "${forwarder}" ; done |
217	if [ "${use_tls}" = "yes" ] ; then
218		echo "        forward-tls-upstream: yes"
219		sed -nE \
220		    -e "s/^${RE_forward_tls}\$/        forward-addr: \\1/p"
221	else
222		sed -nE \
223		    -e "s/^${RE_forward_addr}\$/        forward-addr: \\1/p" \
224		    -e "s/^${RE_forward_name}\$/        forward-host: \\1/p"
225	fi
226}
227
228#
229# Generate lan-zones.conf
230#
231gen_lanzones_conf() {
232	do_not_edit
233	echo "server:"
234	echo "        # Unblock reverse lookups for LAN addresses"
235	echo "        unblock-lan-zones: yes"
236	echo "        insecure-lan-zones: yes"
237}
238
239#
240# Generate control.conf
241#
242gen_control_conf() {
243	do_not_edit
244	echo "remote-control:"
245	echo "        control-enable: yes"
246	echo "        control-interface: ${control_socket}"
247	echo "        control-use-cert: no"
248}
249
250#
251# Generate unbound.conf
252#
253gen_unbound_conf() {
254	do_not_edit
255	echo "server:"
256	echo "        username: ${user}"
257	echo "        directory: ${workdir}"
258	echo "        chroot: ${chrootdir}"
259	echo "        pidfile: ${pidfile}"
260	echo "        auto-trust-anchor-file: ${anchor}"
261	if [ "${use_tls}" = "yes" ] ; then
262		echo "        tls-cert-bundle: /etc/ssl/cert.pem"
263	fi
264	echo "        so-sndbuf: 0"
265	echo ""
266	if [ -f "${forward_conf}" ] ; then
267		echo "include: ${forward_conf}"
268	fi
269	if [ -f "${lanzones_conf}" ] ; then
270		echo "include: ${lanzones_conf}"
271	fi
272	if [ -f "${control_conf}" ] ; then
273		echo "include: ${control_conf}"
274	fi
275	if [ -d "${confdir}" ] ; then
276		echo "include: ${confdir}/*.conf"
277	fi
278}
279
280#
281# Rename a file we are about to replace.
282#
283backup() {
284	local file="$1"
285	if [ -f "${D}${file}" ] ; then
286		local bkfile="${bkdir}/${file##*/}.${bkext}"
287		echo "Original ${file} saved as ${bkfile}"
288		mv "${D}${file}" "${D}${bkfile}"
289	fi
290}
291
292#
293# Wrapper for mktemp which respects DESTDIR
294#
295tmp() {
296	local file="$1"
297	mktemp -u "${D}${file}.XXXXX"
298}
299
300#
301# Replace one file with another, making a backup copy of the first,
302# but only if the new file is different from the old.
303#
304replace() {
305	local file="$1"
306	local newfile="$2"
307	if [ ! -f "${D}${file}" ] ; then
308		echo "${file} created"
309		mv "${newfile}" "${D}${file}"
310	elif ! cmp -s "${D}${file}" "${newfile}" ; then
311		backup "${file}"
312		mv "${newfile}" "${D}${file}"
313	else
314		echo "${file} not modified"
315		rm "${newfile}"
316	fi
317}
318
319#
320# Print usage message and exit
321#
322usage() {
323	exec >&2
324	echo "usage: $self [options] [forwarder ...]"
325	echo "options:"
326	echo "    -n          do not start unbound"
327	echo "    -a path     full path to trust anchor file"
328	echo "    -C path     full path to additional configuration directory"
329	echo "    -c path     full path to unbound configuration file"
330	echo "    -f path     full path to forwarding configuration"
331	echo "    -O path     full path to remote control socket"
332	echo "    -o path     full path to remote control configuration"
333	echo "    -p path     full path to pid file"
334	echo "    -R path     full path to resolvconf.conf"
335	echo "    -r path     full path to resolv.conf"
336	echo "    -s service  name of unbound service"
337	echo "    -u user     user to run unbound as"
338	echo "    -w path     full path to working directory"
339	exit 1
340}
341
342#
343# Main
344#
345main() {
346	umask 022
347
348	#
349	# Parse and validate command-line options
350	#
351	while getopts "a:C:c:f:no:p:R:r:s:tu:w:" option ; do
352		case $option in
353		a)
354			anchor="$OPTARG"
355			;;
356		C)
357			confdir="$OPTARG"
358			;;
359		c)
360			unbound_conf="$OPTARG"
361			;;
362		f)
363			forward_conf="$OPTARG"
364			;;
365		n)
366			start_unbound="no"
367			;;
368		O)
369			control_socket="$OPTARG"
370			;;
371		o)
372			control_conf="$OPTARG"
373			;;
374		p)
375			pidfile="$OPTARG"
376			;;
377		R)
378			resolvconf_conf="$OPTARG"
379			;;
380		r)
381			resolv_conf="$OPTARG"
382			;;
383		s)
384			service="$OPTARG"
385			;;
386		t)
387			use_tls="yes"
388			;;
389		u)
390			user="$OPTARG"
391			;;
392		w)
393			workdir="$OPTARG"
394			;;
395		*)
396			usage
397			;;
398		esac
399	done
400	shift $((OPTIND-1))
401	set_defaults
402
403	#
404	# Get the list of forwarders, either from the command line or
405	# from resolv.conf.
406	#
407	forwarders="$@"
408	case "${forwarders}" in
409	[Nn][Oo][Nn][Ee])
410		forwarders="none"
411		style=recursing
412		;;
413	"")
414		if [ -f "${D}${resolv_conf}" ] ; then
415			echo "Extracting forwarders from ${resolv_conf}."
416			forwarders=$(get_nameservers <"${D}${resolv_conf}")
417		fi
418		style=dynamic
419		;;
420	*)
421		style=static
422		;;
423	esac
424
425	#
426	# Generate forward.conf.
427	#
428	if [ -z "${forwarders}" ] ; then
429		echo -n "No forwarders found in ${resolv_conf##*/}, "
430		if [ -f "${forward_conf}" ] ; then
431			echo "using existing ${forward_conf##*/}."
432		else
433			echo "unbound will recurse."
434		fi
435	elif [ "${forwarders}" = "none" ] ; then
436		echo "Forwarding disabled, unbound will recurse."
437		backup "${forward_conf}"
438	else
439		local tmp_forward_conf=$(tmp "${forward_conf}")
440		gen_forward_conf ${forwarders} | unexpand >"${tmp_forward_conf}"
441		replace "${forward_conf}" "${tmp_forward_conf}"
442	fi
443
444	#
445	# Generate lan-zones.conf.
446	#
447	local tmp_lanzones_conf=$(tmp "${lanzones_conf}")
448	gen_lanzones_conf | unexpand >"${tmp_lanzones_conf}"
449	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
450
451	#
452	# Generate control.conf.
453	#
454	local tmp_control_conf=$(tmp "${control_conf}")
455	gen_control_conf | unexpand >"${tmp_control_conf}"
456	replace "${control_conf}" "${tmp_control_conf}"
457
458	#
459	# Generate unbound.conf.
460	#
461	local tmp_unbound_conf=$(tmp "${unbound_conf}")
462	set_chrootdir
463	gen_unbound_conf | unexpand >"${tmp_unbound_conf}"
464	replace "${unbound_conf}" "${tmp_unbound_conf}"
465
466	#
467	# Start unbound, unless requested not to.  Stop immediately if
468	# it is not enabled so we don't end up with a resolv.conf that
469	# points into nothingness.  We could "onestart" it, but it
470	# wouldn't stick.
471	#
472	if [ "${start_unbound}" = "no" ] ; then
473		# skip
474	elif ! service "${service}" enabled ; then
475		echo "Please enable $service in rc.conf(5) and try again."
476		return 1
477	elif ! service "${service}" restart ; then
478		echo "Failed to start $service."
479		return 1
480	fi
481
482	#
483	# Rewrite resolvconf.conf so resolvconf updates forward.conf
484	# instead of resolv.conf.
485	#
486	local tmp_resolvconf_conf=$(tmp "${resolvconf_conf}")
487	gen_resolvconf_conf "${style}" | unexpand >"${tmp_resolvconf_conf}"
488	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
489
490	#
491	# Finally, rewrite resolv.conf.
492	#
493	local tmp_resolv_conf=$(tmp "${resolv_conf}")
494	gen_resolv_conf <"${D}${resolv_conf}" | unexpand >"${tmp_resolv_conf}"
495	replace "${resolv_conf}" "${tmp_resolv_conf}"
496}
497
498main "$@"
499