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-system-cert: yes"
263	fi
264	echo ""
265	if [ -f "${forward_conf}" ] ; then
266		echo "include: ${forward_conf}"
267	fi
268	if [ -f "${lanzones_conf}" ] ; then
269		echo "include: ${lanzones_conf}"
270	fi
271	if [ -f "${control_conf}" ] ; then
272		echo "include: ${control_conf}"
273	fi
274	if [ -d "${confdir}" ] ; then
275		echo "include: ${confdir}/*.conf"
276	fi
277}
278
279#
280# Rename a file we are about to replace.
281#
282backup() {
283	local file="$1"
284	if [ -f "${D}${file}" ] ; then
285		local bkfile="${bkdir}/${file##*/}.${bkext}"
286		echo "Original ${file} saved as ${bkfile}"
287		mv "${D}${file}" "${D}${bkfile}"
288	fi
289}
290
291#
292# Wrapper for mktemp which respects DESTDIR
293#
294tmp() {
295	local file="$1"
296	mktemp -u "${D}${file}.XXXXX"
297}
298
299#
300# Replace one file with another, making a backup copy of the first,
301# but only if the new file is different from the old.
302#
303replace() {
304	local file="$1"
305	local newfile="$2"
306	if [ ! -f "${D}${file}" ] ; then
307		echo "${file} created"
308		mv "${newfile}" "${D}${file}"
309	elif ! cmp -s "${D}${file}" "${newfile}" ; then
310		backup "${file}"
311		mv "${newfile}" "${D}${file}"
312	else
313		echo "${file} not modified"
314		rm "${newfile}"
315	fi
316}
317
318#
319# Print usage message and exit
320#
321usage() {
322	exec >&2
323	echo "usage: $self [options] [forwarder ...]"
324	echo "options:"
325	echo "    -n          do not start unbound"
326	echo "    -a path     full path to trust anchor file"
327	echo "    -C path     full path to additional configuration directory"
328	echo "    -c path     full path to unbound configuration file"
329	echo "    -f path     full path to forwarding configuration"
330	echo "    -O path     full path to remote control socket"
331	echo "    -o path     full path to remote control configuration"
332	echo "    -p path     full path to pid file"
333	echo "    -R path     full path to resolvconf.conf"
334	echo "    -r path     full path to resolv.conf"
335	echo "    -s service  name of unbound service"
336	echo "    -u user     user to run unbound as"
337	echo "    -w path     full path to working directory"
338	exit 1
339}
340
341#
342# Main
343#
344main() {
345	umask 022
346
347	#
348	# Parse and validate command-line options
349	#
350	while getopts "a:C:c:f:no:p:R:r:s:tu:w:" option ; do
351		case $option in
352		a)
353			anchor="$OPTARG"
354			;;
355		C)
356			confdir="$OPTARG"
357			;;
358		c)
359			unbound_conf="$OPTARG"
360			;;
361		f)
362			forward_conf="$OPTARG"
363			;;
364		n)
365			start_unbound="no"
366			;;
367		O)
368			control_socket="$OPTARG"
369			;;
370		o)
371			control_conf="$OPTARG"
372			;;
373		p)
374			pidfile="$OPTARG"
375			;;
376		R)
377			resolvconf_conf="$OPTARG"
378			;;
379		r)
380			resolv_conf="$OPTARG"
381			;;
382		s)
383			service="$OPTARG"
384			;;
385		t)
386			use_tls="yes"
387			;;
388		u)
389			user="$OPTARG"
390			;;
391		w)
392			workdir="$OPTARG"
393			;;
394		*)
395			usage
396			;;
397		esac
398	done
399	shift $((OPTIND-1))
400	set_defaults
401
402	#
403	# Get the list of forwarders, either from the command line or
404	# from resolv.conf.
405	#
406	forwarders="$@"
407	case "${forwarders}" in
408	[Nn][Oo][Nn][Ee])
409		forwarders="none"
410		style=recursing
411		;;
412	"")
413		if [ -f "${D}${resolv_conf}" ] ; then
414			echo "Extracting forwarders from ${resolv_conf}."
415			forwarders=$(get_nameservers <"${D}${resolv_conf}")
416		fi
417		style=dynamic
418		;;
419	*)
420		style=static
421		;;
422	esac
423
424	#
425	# Generate forward.conf.
426	#
427	if [ -z "${forwarders}" ] ; then
428		echo -n "No forwarders found in ${resolv_conf##*/}, "
429		if [ -f "${forward_conf}" ] ; then
430			echo "using existing ${forward_conf##*/}."
431		else
432			echo "unbound will recurse."
433		fi
434	elif [ "${forwarders}" = "none" ] ; then
435		echo "Forwarding disabled, unbound will recurse."
436		backup "${forward_conf}"
437	else
438		local tmp_forward_conf=$(tmp "${forward_conf}")
439		gen_forward_conf ${forwarders} | unexpand >"${tmp_forward_conf}"
440		replace "${forward_conf}" "${tmp_forward_conf}"
441	fi
442
443	#
444	# Generate lan-zones.conf.
445	#
446	local tmp_lanzones_conf=$(tmp "${lanzones_conf}")
447	gen_lanzones_conf | unexpand >"${tmp_lanzones_conf}"
448	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
449
450	#
451	# Generate control.conf.
452	#
453	local tmp_control_conf=$(tmp "${control_conf}")
454	gen_control_conf | unexpand >"${tmp_control_conf}"
455	replace "${control_conf}" "${tmp_control_conf}"
456
457	#
458	# Generate unbound.conf.
459	#
460	local tmp_unbound_conf=$(tmp "${unbound_conf}")
461	set_chrootdir
462	gen_unbound_conf | unexpand >"${tmp_unbound_conf}"
463	replace "${unbound_conf}" "${tmp_unbound_conf}"
464
465	#
466	# Start unbound, unless requested not to.  Stop immediately if
467	# it is not enabled so we don't end up with a resolv.conf that
468	# points into nothingness.  We could "onestart" it, but it
469	# wouldn't stick.
470	#
471	if [ "${start_unbound}" = "no" ] ; then
472		# skip
473	elif ! service "${service}" enabled ; then
474		echo "Please enable $service in rc.conf(5) and try again."
475		return 1
476	elif ! service "${service}" restart ; then
477		echo "Failed to start $service."
478		return 1
479	fi
480
481	#
482	# Rewrite resolvconf.conf so resolvconf updates forward.conf
483	# instead of resolv.conf.
484	#
485	local tmp_resolvconf_conf=$(tmp "${resolvconf_conf}")
486	gen_resolvconf_conf "${style}" | unexpand >"${tmp_resolvconf_conf}"
487	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
488
489	#
490	# Finally, rewrite resolv.conf.
491	#
492	local tmp_resolv_conf=$(tmp "${resolv_conf}")
493	gen_resolv_conf <"${D}${resolv_conf}" | unexpand >"${tmp_resolv_conf}"
494	replace "${resolv_conf}" "${tmp_resolv_conf}"
495}
496
497main "$@"
498