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$
30#
31
32D="${DESTDIR}"
33echo "destination: ${D}"
34
35#
36# Configuration variables
37#
38user=""
39unbound_conf=""
40forward_conf=""
41lanzones_conf=""
42control_conf=""
43control_socket=""
44workdir=""
45confdir=""
46chrootdir=""
47anchor=""
48pidfile=""
49resolv_conf=""
50resolvconf_conf=""
51service=""
52start_unbound=""
53use_tls=""
54forwarders=""
55
56#
57# Global variables
58#
59self=$(basename $(realpath "$0"))
60bkdir=/var/backups
61bkext=$(date "+%Y%m%d.%H%M%S")
62
63#
64# Regular expressions
65#
66RE_octet="([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])"
67RE_ipv4="(${RE_octet}(\\.${RE_octet}){3})"
68RE_word="([0-9A-Fa-f]{1,4})"
69RE_ipv6="((${RE_word}:){1,}(:|(:${RE_word})*)|::1)"
70RE_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]))))"
71RE_dnsname="([0-9A-Za-z-]{1,}(\\.[0-9A-Za-z-]{1,})*\\.?)"
72RE_forward_addr="((${RE_ipv4}|${RE_ipv6})(@${RE_port})?)"
73RE_forward_name="(${RE_dnsname}(@${RE_port})?)"
74RE_forward_tls="(${RE_forward_addr}(#${RE_dnsname})?)"
75
76#
77# Set default values for unset configuration variables.
78#
79set_defaults() {
80	: ${user:=unbound}
81	: ${workdir:=/var/unbound}
82	: ${confdir:=${workdir}/conf.d}
83	: ${unbound_conf:=${workdir}/unbound.conf}
84	: ${forward_conf:=${workdir}/forward.conf}
85	: ${lanzones_conf:=${workdir}/lan-zones.conf}
86	: ${control_conf:=${workdir}/control.conf}
87	: ${control_socket:=/var/run/local_unbound.ctl}
88	: ${anchor:=${workdir}/root.key}
89	: ${pidfile:=/var/run/local_unbound.pid}
90	: ${resolv_conf:=/etc/resolv.conf}
91	: ${resolvconf_conf:=/etc/resolvconf.conf}
92	: ${service:=local_unbound}
93	: ${start_unbound:=yes}
94	: ${use_tls:=no}
95}
96
97#
98# Verify that the configuration files are inside the working
99# directory, and if so, set the chroot directory accordingly.
100#
101set_chrootdir() {
102	chrootdir="${workdir}"
103	for file in "${unbound_conf}" "${forward_conf}" \
104	    "${lanzones_conf}" "${control_conf}" "${anchor}" ; do
105		if [ "${file#${workdir%/}/}" = "${file}" ] ; then
106			echo "warning: ${file} is outside ${workdir}" >&2
107			chrootdir=""
108		fi
109	done
110	if [ -z "${chrootdir}" ] ; then
111		echo "warning: disabling chroot" >&2
112	fi
113}
114
115#
116# Scan through /etc/resolv.conf looking for uncommented nameserver
117# lines that don't point to localhost and return their values.
118#
119get_nameservers() {
120	while read line ; do
121		local bareline=${line%%\#*}
122		local key=${bareline%% *}
123		local value=${bareline#* }
124		case ${key} in
125		nameserver)
126			case ${value} in
127			127.0.0.1|::1|localhost|localhost.*)
128				;;
129			*)
130				echo "${value}"
131				;;
132			esac
133			;;
134		esac
135	done
136}
137
138#
139# Scan through /etc/resolv.conf looking for uncommented nameserver
140# lines.  Comment out any that don't point to localhost.  Finally,
141# append a nameserver line that points to localhost, if there wasn't
142# one already, and enable the edns0 option.
143#
144gen_resolv_conf() {
145	local localhost=no
146	local edns0=no
147	while read line ; do
148		local bareline=${line%%\#*}
149		local key=${bareline%% *}
150		local value=${bareline#* }
151		case ${key} in
152		nameserver)
153			case ${value} in
154			127.0.0.1|::1|localhost|localhost.*)
155				localhost=yes
156				;;
157			*)
158				echo -n "# "
159				;;
160			esac
161			;;
162		options)
163			case ${value} in
164			*edns0*)
165				edns0=yes
166				;;
167			esac
168			;;
169		esac
170		echo "${line}"
171	done
172	if [ "${localhost}" = "no" ] ; then
173		echo "nameserver 127.0.0.1"
174	fi
175	if [ "${edns0}" = "no" ] ; then
176		echo "options edns0"
177	fi
178}
179
180#
181# Boilerplate
182#
183do_not_edit() {
184	echo "# This file was generated by $self."
185	echo "# Modifications will be overwritten."
186}
187
188#
189# Generate resolvconf.conf so it updates forward.conf in addition to
190# resolv.conf.  Note "in addition to" rather than "instead of",
191# because we still want it to update the domain name and search path
192# if they change.  Setting name_servers to "127.0.0.1" ensures that
193# the libc resolver will try unbound first.
194#
195gen_resolvconf_conf() {
196	local style="$1"
197	do_not_edit
198	echo "resolv_conf=\"/dev/null\" # prevent updating ${resolv_conf}"
199	if [ "${style}" = "dynamic" ] ; then
200		echo "unbound_conf=\"${forward_conf}\""
201		echo "unbound_pid=\"${pidfile}\""
202		echo "unbound_service=\"${service}\""
203		# resolvconf(8) likes to restart rather than reload
204		echo "unbound_restart=\"service ${service} reload\""
205	else
206		echo "# Static DNS configuration"
207	fi
208}
209
210#
211# Generate forward.conf
212#
213gen_forward_conf() {
214	do_not_edit
215	echo "forward-zone:"
216	echo "        name: ."
217	for forwarder ; do echo "${forwarder}" ; done |
218	if [ "${use_tls}" = "yes" ] ; then
219		echo "        forward-tls-upstream: yes"
220		sed -nE \
221		    -e "s/^(${RE_forward_tls})$/        forward-addr: \\1/p"
222	else
223		sed -nE \
224		    -e "s/^${RE_forward_addr}\$/        forward-addr: \\1/p" \
225		    -e "s/^${RE_forward_name}\$/        forward-host: \\1/p"
226	fi
227}
228
229#
230# Generate lan-zones.conf
231#
232gen_lanzones_conf() {
233	do_not_edit
234	echo "server:"
235	echo "        # Unblock reverse lookups for LAN addresses"
236	echo "        unblock-lan-zones: yes"
237	echo "        insecure-lan-zones: yes"
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	if [ "${use_tls}" = "yes" ] ; then
263		echo "        tls-cert-bundle: /etc/ssl/cert.pem"
264	fi
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		echo "Extracting forwarders from ${resolv_conf}."
415		forwarders=$(get_nameservers <"${D}${resolv_conf}")
416		style=dynamic
417		;;
418	*)
419		style=static
420		;;
421	esac
422
423	#
424	# Generate forward.conf.
425	#
426	if [ -z "${forwarders}" ] ; then
427		echo -n "No forwarders found in ${resolv_conf##*/}, "
428		if [ -f "${forward_conf}" ] ; then
429			echo "using existing ${forward_conf##*/}."
430		else
431			echo "unbound will recurse."
432		fi
433	elif [ "${forwarders}" = "none" ] ; then
434		echo "Forwarding disabled, unbound will recurse."
435		backup "${forward_conf}"
436	else
437		local tmp_forward_conf=$(tmp "${forward_conf}")
438		gen_forward_conf ${forwarders} | unexpand >"${tmp_forward_conf}"
439		replace "${forward_conf}" "${tmp_forward_conf}"
440	fi
441
442	#
443	# Generate lan-zones.conf.
444	#
445	local tmp_lanzones_conf=$(tmp "${lanzones_conf}")
446	gen_lanzones_conf | unexpand >"${tmp_lanzones_conf}"
447	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
448
449	#
450	# Generate control.conf.
451	#
452	local tmp_control_conf=$(tmp "${control_conf}")
453	gen_control_conf | unexpand >"${tmp_control_conf}"
454	replace "${control_conf}" "${tmp_control_conf}"
455
456	#
457	# Generate unbound.conf.
458	#
459	local tmp_unbound_conf=$(tmp "${unbound_conf}")
460	set_chrootdir
461	gen_unbound_conf | unexpand >"${tmp_unbound_conf}"
462	replace "${unbound_conf}" "${tmp_unbound_conf}"
463
464	#
465	# Start unbound, unless requested not to.  Stop immediately if
466	# it is not enabled so we don't end up with a resolv.conf that
467	# points into nothingness.  We could "onestart" it, but it
468	# wouldn't stick.
469	#
470	if [ "${start_unbound}" = "no" ] ; then
471		# skip
472	elif ! service "${service}" enabled ; then
473		echo "Please enable $service in rc.conf(5) and try again."
474		return 1
475	elif ! service "${service}" restart ; then
476		echo "Failed to start $service."
477		return 1
478	fi
479
480	#
481	# Rewrite resolvconf.conf so resolvconf updates forward.conf
482	# instead of resolv.conf.
483	#
484	local tmp_resolvconf_conf=$(tmp "${resolvconf_conf}")
485	gen_resolvconf_conf "${style}" | unexpand >"${tmp_resolvconf_conf}"
486	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
487
488	#
489	# Finally, rewrite resolv.conf.
490	#
491	local tmp_resolv_conf=$(tmp "${resolv_conf}")
492	gen_resolv_conf <"${D}${resolv_conf}" | unexpand >"${tmp_resolv_conf}"
493	replace "${resolv_conf}" "${tmp_resolv_conf}"
494}
495
496main "$@"
497