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$
28#
29
30#
31# Configuration variables
32#
33user=""
34unbound_conf=""
35forward_conf=""
36lanzones_conf=""
37workdir=""
38confdir=""
39chrootdir=""
40anchor=""
41pidfile=""
42resolv_conf=""
43resolvconf_conf=""
44service=""
45start_unbound=""
46forwarders=""
47
48#
49# Global variables
50#
51self=$(basename $(realpath "$0"))
52bkext=$(date "+%Y%m%d.%H%M%S")
53
54#
55# Set default values for unset configuration variables.
56#
57set_defaults() {
58	: ${user:=unbound}
59	: ${workdir:=/var/unbound}
60	: ${confdir:=${workdir}/conf.d}
61	: ${unbound_conf:=${workdir}/unbound.conf}
62	: ${forward_conf:=${workdir}/forward.conf}
63	: ${lanzones_conf:=${workdir}/lan-zones.conf}
64	: ${anchor:=${workdir}/root.key}
65	: ${pidfile:=/var/run/local_unbound.pid}
66	: ${resolv_conf:=/etc/resolv.conf}
67	: ${resolvconf_conf:=/etc/resolvconf.conf}
68	: ${service:=local_unbound}
69	: ${start_unbound:=yes}
70}
71
72#
73# Verify that the configuration files are inside the working
74# directory, and if so, set the chroot directory accordingly.
75#
76set_chrootdir() {
77	chrootdir="${workdir}"
78	for file in "${unbound_conf}" "${forward_conf}" \
79	    "${lanzones_conf}" "${anchor}" ; do
80		if [ "${file#${workdir%/}/}" = "${file}" ] ; then
81			echo "warning: ${file} is outside ${workdir}" >&2
82			chrootdir=""
83		fi
84	done
85	if [ -z "${chrootdir}" ] ; then
86		echo "warning: disabling chroot" >&2
87	fi
88}
89
90#
91# Scan through /etc/resolv.conf looking for uncommented nameserver
92# lines that don't point to localhost and return their values.
93#
94get_nameservers() {
95	while read line ; do
96		local bareline=${line%%\#*}
97		local key=${bareline%% *}
98		local value=${bareline#* }
99		case ${key} in
100		nameserver)
101			case ${value} in
102			127.0.0.1|::1|localhost|localhost.*)
103				;;
104			*)
105				echo "${value}"
106				;;
107			esac
108			;;
109		esac
110	done
111}
112
113#
114# Scan through /etc/resolv.conf looking for uncommented nameserver
115# lines.  Comment out any that don't point to localhost.  Finally,
116# append a nameserver line that points to localhost, if there wasn't
117# one already, and enable the edns0 option.
118#
119gen_resolv_conf() {
120	local localhost=no
121	local edns0=no
122	while read line ; do
123		local bareline=${line%%\#*}
124		local key=${bareline%% *}
125		local value=${bareline#* }
126		case ${key} in
127		nameserver)
128			case ${value} in
129			127.0.0.1|::1|localhost|localhost.*)
130				localhost=yes
131				;;
132			*)
133				echo -n "# "
134				;;
135			esac
136			;;
137		options)
138			case ${value} in
139			*edns0*)
140				edns0=yes
141				;;
142			esac
143			;;
144		esac
145		echo "${line}"
146	done
147	if [ "${localhost}" = "no" ] ; then
148		echo "nameserver 127.0.0.1"
149	fi
150	if [ "${edns0}" = "no" ] ; then
151		echo "options edns0"
152	fi
153}
154
155#
156# Generate resolvconf.conf so it updates forward.conf in addition to
157# resolv.conf.  Note "in addition to" rather than "instead of",
158# because we still want it to update the domain name and search path
159# if they change.  Setting name_servers to "127.0.0.1" ensures that
160# the libc resolver will try unbound first.
161#
162gen_resolvconf_conf() {
163	echo "# Generated by $self"
164	echo "resolv_conf=\"/dev/null\" # prevent updating ${resolv_conf}"
165	echo "unbound_conf=\"${forward_conf}\""
166	echo "unbound_pid=\"${pidfile}\""
167	echo "unbound_service=\"${service}\""
168	# resolvconf(8) likes to restart rather than reload
169	echo "unbound_restart=\"service ${service} reload\""
170}
171
172#
173# Generate forward.conf
174#
175gen_forward_conf() {
176	echo "# Generated by $self"
177	echo "# Do not edit this file."
178	echo "forward-zone:"
179	echo "        name: ."
180	for forwarder ; do
181		if expr "${forwarder}" : "^[0-9A-Fa-f:.]\{1,\}$" >/dev/null ; then
182			echo "        forward-addr: ${forwarder}"
183		else
184			echo "        forward-host: ${forwarder}"
185		fi
186	done
187}
188
189#
190# Generate lan-zones.conf
191#
192gen_lanzones_conf() {
193	echo "# Generated by $self"
194	echo "# Do not edit this file."
195	echo "server:"
196	echo "        # Unblock reverse lookups for LAN addresses"
197	echo "        unblock-lan-zones: yes"
198	echo "        domain-insecure: 10.in-addr.arpa."
199	echo "        domain-insecure: 127.in-addr.arpa."
200	echo "        domain-insecure: 16.172.in-addr.arpa."
201	echo "        domain-insecure: 17.172.in-addr.arpa."
202	echo "        domain-insecure: 18.172.in-addr.arpa."
203	echo "        domain-insecure: 19.172.in-addr.arpa."
204	echo "        domain-insecure: 20.172.in-addr.arpa."
205	echo "        domain-insecure: 21.172.in-addr.arpa."
206	echo "        domain-insecure: 22.172.in-addr.arpa."
207	echo "        domain-insecure: 23.172.in-addr.arpa."
208	echo "        domain-insecure: 24.172.in-addr.arpa."
209	echo "        domain-insecure: 25.172.in-addr.arpa."
210	echo "        domain-insecure: 26.172.in-addr.arpa."
211	echo "        domain-insecure: 27.172.in-addr.arpa."
212	echo "        domain-insecure: 28.172.in-addr.arpa."
213	echo "        domain-insecure: 29.172.in-addr.arpa."
214	echo "        domain-insecure: 30.172.in-addr.arpa."
215	echo "        domain-insecure: 31.172.in-addr.arpa."
216	echo "        domain-insecure: 168.192.in-addr.arpa."
217	echo "        domain-insecure: 254.169.in-addr.arpa."
218	echo "        domain-insecure: d.f.ip6.arpa."
219	echo "        domain-insecure: 8.e.ip6.arpa."
220	echo "        domain-insecure: 9.e.ip6.arpa."
221	echo "        domain-insecure: a.e.ip6.arpa."
222	echo "        domain-insecure: b.e.ip6.arpa."
223}
224
225#
226# Generate unbound.conf
227#
228gen_unbound_conf() {
229	echo "# Generated by $self"
230	echo "server:"
231	echo "        username: ${user}"
232	echo "        directory: ${workdir}"
233	echo "        chroot: ${chrootdir}"
234	echo "        pidfile: ${pidfile}"
235	echo "        auto-trust-anchor-file: ${anchor}"
236	echo ""
237	if [ -f "${forward_conf}" ] ; then
238		echo "include: ${forward_conf}"
239	fi
240	if [ -f "${lanzones_conf}" ] ; then
241		echo "include: ${lanzones_conf}"
242	fi
243	if [ -d "${confdir}" ] ; then
244		echo "include: ${confdir}/*.conf"
245	fi
246}
247
248#
249# Replace one file with another, making a backup copy of the first,
250# but only if the new file is different from the old.
251#
252replace() {
253	local file="$1"
254	local newfile="$2"
255	if [ ! -f "${file}" ] ; then
256		echo "${file} created"
257		mv "${newfile}" "${file}"
258	elif ! cmp -s "${file}" "${newfile}" ; then
259		local oldfile="${file}.${bkext}"
260		echo "original ${file} saved as ${oldfile}"
261		mv "${file}" "${oldfile}"
262		mv "${newfile}" "${file}"
263	else
264		echo "${file} not modified"
265		rm "${newfile}"
266	fi
267}
268
269#
270# Print usage message and exit
271#
272usage() {
273	exec >&2
274	echo "usage: $self [options] [forwarder ...]"
275	echo "options:"
276	echo "    -n          do not start unbound"
277	echo "    -a path     full path to trust anchor file"
278	echo "    -C path     full path to additional configuration directory"
279	echo "    -c path     full path to unbound configuration file"
280	echo "    -f path     full path to forwarding configuration"
281	echo "    -p path     full path to pid file"
282	echo "    -R path     full path to resolvconf.conf"
283	echo "    -r path     full path to resolv.conf"
284	echo "    -s service  name of unbound service"
285	echo "    -u user     user to run unbound as"
286	echo "    -w path     full path to working directory"
287	exit 1
288}
289
290#
291# Main
292#
293main() {
294	umask 022
295
296	#
297	# Parse and validate command-line options
298	#
299	while getopts "a:C:c:f:np:R:r:s:u:w:" option ; do
300		case $option in
301		a)
302			anchor="$OPTARG"
303			;;
304		C)
305			confdir="$OPTARG"
306			;;
307		c)
308			unbound_conf="$OPTARG"
309			;;
310		f)
311			forward_conf="$OPTARG"
312			;;
313		n)
314			start_unbound="no"
315			;;
316		p)
317			pidfile="$OPTARG"
318			;;
319		R)
320			resolvconf_conf="$OPTARG"
321			;;
322		r)
323			resolv_conf="$OPTARG"
324			;;
325		s)
326			service="$OPTARG"
327			;;
328		u)
329			user="$OPTARG"
330			;;
331		w)
332			workdir="$OPTARG"
333			;;
334		*)
335			usage
336			;;
337		esac
338	done
339	shift $((OPTIND-1))
340	set_defaults
341
342	#
343	# Get the list of forwarders, either from the command line or
344	# from resolv.conf.
345	#
346	forwarders="$@"
347	if [ -z "$forwarders" ] ; then
348		echo "Extracting forwarders from ${resolv_conf}."
349		forwarders=$(get_nameservers <"${resolv_conf}")
350	fi
351
352	#
353	# Generate forward.conf.
354	#
355	if [ -z "${forwarders}" ] ; then
356		echo -n "No forwarders found in ${resolv_conf##*/}, "
357		if [ -f "${forward_conf}" ] ; then
358			echo "using existing ${forward_conf##*/}."
359		else
360			echo "unbound will recurse."
361		fi
362	else
363		local tmp_forward_conf=$(mktemp -u "${forward_conf}.XXXXX")
364		gen_forward_conf ${forwarders} >"${tmp_forward_conf}"
365		replace "${forward_conf}" "${tmp_forward_conf}"
366	fi
367
368	#
369	# Generate lan-zones.conf.
370	#
371	local tmp_lanzones_conf=$(mktemp -u "${lanzones_conf}.XXXXX")
372	gen_lanzones_conf >"${tmp_lanzones_conf}"
373	replace "${lanzones_conf}" "${tmp_lanzones_conf}"
374
375	#
376	# Generate unbound.conf.
377	#
378	local tmp_unbound_conf=$(mktemp -u "${unbound_conf}.XXXXX")
379	set_chrootdir
380	gen_unbound_conf >"${tmp_unbound_conf}"
381	replace "${unbound_conf}" "${tmp_unbound_conf}"
382
383	#
384	# Start unbound, unless requested not to.  Stop immediately if
385	# it is not enabled so we don't end up with a resolv.conf that
386	# points into nothingness.  We could "onestart" it, but it
387	# wouldn't stick.
388	#
389	if [ "${start_unbound}" = "no" ] ; then
390		# skip
391	elif ! service "${service}" enabled ; then
392		echo "Please enable $service in rc.conf(5) and try again."
393		return 1
394	elif ! service "${service}" restart ; then
395		echo "Failed to start $service."
396		return 1
397	fi
398
399	#
400	# Rewrite resolvconf.conf so resolvconf updates forward.conf
401	# instead of resolv.conf.
402	#
403	local tmp_resolvconf_conf=$(mktemp -u "${resolvconf_conf}.XXXXX")
404	gen_resolvconf_conf >"${tmp_resolvconf_conf}"
405	replace "${resolvconf_conf}" "${tmp_resolvconf_conf}"
406
407	#
408	# Finally, rewrite resolv.conf.
409	#
410	local tmp_resolv_conf=$(mktemp -u "${resolv_conf}.XXXXX")
411	gen_resolv_conf <"${resolv_conf}" >"${tmp_resolv_conf}"
412	replace "${resolv_conf}" "${tmp_resolv_conf}"
413}
414
415main "$@"
416