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 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