Ensure that creating interface resolv.conf files is an atomic operation.
[openresolv] / resolvconf.in
1 #!/bin/sh
2 # Copyright (c) 2007-2012 Roy Marples
3 # All rights reserved
4
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 #     * Redistributions of source code must retain the above copyright
9 #       notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 #       copyright notice, this list of conditions and the following
12 #       disclaimer in the documentation and/or other materials provided
13 #       with the distribution.
14 #
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27 RESOLVCONF="$0"
28 SYSCONFDIR=@SYSCONFDIR@
29 LIBEXECDIR=@LIBEXECDIR@
30 VARDIR=@VARDIR@
31
32 # Disregard dhcpcd setting
33 unset interface_order state_dir
34
35 # Support original resolvconf configuration layout
36 # as well as the openresolv config file
37 if [ -f "$SYSCONFDIR"/resolvconf.conf ]; then
38         . "$SYSCONFDIR"/resolvconf.conf
39         [ -n "$state_dir" ] && VARDIR="$state_dir"
40 elif [ -d "$SYSCONFDIR/resolvconf" ]; then
41         SYSCONFDIR="$SYSCONFDIR/resolvconf"
42         if [ -f "$SYSCONFDIR"/interface-order ]; then
43                 interface_order="$(cat "$SYSCONFDIR"/interface-order)"
44         fi
45 fi
46 TMPDIR="$VARDIR/tmp"
47 IFACEDIR="$VARDIR/interfaces"
48 METRICDIR="$VARDIR/metrics"
49 PRIVATEDIR="$VARDIR/private"
50
51 : ${dynamic_order:=tap[0-9]* tun[0-9]* vpn vpn[0-9]* ppp[0-9]* ippp[0-9]*}
52 : ${interface_order:=lo lo[0-9]*}
53 : ${name_server_blacklist:=0.0.0.0}
54
55 error_exit()
56 {
57         echo "$*" >&2
58         exit 1
59 }
60
61 usage()
62 {
63         cat <<-EOF
64         Usage: ${RESOLVCONF##*/} [options]
65
66         Inform the system about any DNS updates.
67
68         Options:
69           -a \$INTERFACE    Add DNS information to the specified interface
70                            (DNS supplied via stdin in resolv.conf format)
71           -m metric        Give the added DNS information a metric
72           -p               Mark the interface as private
73           -d \$INTERFACE    Delete DNS information from the specified interface
74           -f               Ignore non existant interfaces
75           -I               Init the state dir
76           -u               Run updates from our current DNS information
77           -l [\$PATTERN]    Show DNS information, optionally from interfaces
78                            that match the specified pattern
79           -i [\$PATTERN]    Show interfaces that have supplied DNS information
80                    optionally from interfaces that match the specified
81                    pattern
82           -v [\$PATTERN]    echo NEWDOMAIN, NEWSEARCH and NEWNS variables to
83                            the console
84           -h               Show this help cruft
85         EOF
86         [ -z "$1" ] && exit 0
87         echo
88         error_exit "$*"
89 }
90
91 echo_resolv()
92 {
93         local line= OIFS="$IFS"
94
95         [ -n "$1" -a -e "$IFACEDIR/$1" ] || return 1
96         echo "# resolv.conf from $1"
97         # Our variable maker works of the fact each resolv.conf per interface
98         # is separated by blank lines.
99         # So we remove them when echoing them.
100         while read -r line; do
101                 IFS="$OIFS"
102                 if [ -n "$line" ]; then
103                         # We need to set IFS here to preserve any whitespace
104                         IFS=''
105                         printf "%s\n" "$line"
106                 fi
107         done < "$IFACEDIR/$1"
108         echo
109         IFS="$OIFS"
110 }
111
112 # Parse resolv.conf's and make variables
113 # for domain name servers, search name servers and global nameservers
114 parse_resolv()
115 {
116         local line= ns= ds= search= d= n= newns=
117         local new=true iface= private=false p= domain=
118
119         newns=
120
121         while read -r line; do
122                 case "$line" in
123                 "# resolv.conf from "*)
124                         if ${new}; then
125                                 iface="${line#\# resolv.conf from *}"
126                                 new=false
127                                 if [ -e "$PRIVATEDIR/$iface" ]; then
128                                         private=true
129                                 else
130                                         # Allow expansion
131                                         cd "$IFACEDIR"
132                                         private=false
133                                         for p in $private_interfaces; do
134                                                 case "$iface" in
135                                                 "$p"|"$p":*) private=true; break;;
136                                                 esac
137                                         done
138                                 fi
139                         fi
140                         ;;
141                 "nameserver "*)
142                         case "${line#* }" in
143                         127.*|0.0.0.0|255.255.255.255|::1)
144                                 echo "LOCALNAMESERVERS=\"\$LOCALNAMESERVERS ${line#* }\""
145                                 continue
146                                 ;;
147                         esac
148                         ns="$ns${line#* } "
149                         ;;
150                 "domain "*)
151                         if [ -z "$domain" ]; then
152                                 domain="${line#* }"
153                                 echo "DOMAIN=\"$domain\""
154                         fi
155                         search="${line#* }"
156                         ;;
157                 "search "*)
158                         search="${line#* }"
159                         ;;
160                 *)
161                         [ -n "$line" ] && continue
162                         if [ -n "$ns" -a -n "$search" ]; then
163                                 newns=
164                                 for n in $ns; do
165                                         newns="$newns${newns:+,}$n"
166                                 done
167                                 ds=
168                                 for d in $search; do
169                                         ds="$ds${ds:+ }$d:$newns"
170                                 done
171                                 echo "DOMAINS=\"\$DOMAINS $ds\""
172                         fi
173                         echo "SEARCH=\"\$SEARCH $search\""
174                         if ! $private; then
175                                 echo "NAMESERVERS=\"\$NAMESERVERS $ns\""
176                         fi
177                         ns=
178                         search=
179                         new=true
180                         ;;
181                 esac
182         done
183 }
184
185 uniqify()
186 {
187         local result=
188         while [ -n "$1" ]; do
189                 case " $result " in
190                 *" $1 "*);;
191                 *) result="$result $1";;
192                 esac
193                 shift
194         done
195         echo "${result# *}"
196 }
197
198 dirname()
199 {
200         local dir= OIFS="$IFS"
201         local IFS=/
202         set -- $@
203         IFS="$OIFS"
204         if [ -n "$1" ]; then
205                 printf %s .
206         else
207                 shift
208         fi
209         while [ -n "$2" ]; do
210                 printf "/%s" "$1"
211                 shift
212         done
213         printf "\n"
214 }
215
216 config_mkdirs()
217 {
218         local e=0 f d
219         for f; do
220                 [ -n "$f" ] || continue
221                 d="$(dirname "$f")"
222                 if [ ! -d "$d" ]; then
223                         if type install >/dev/null 2>&1; then
224                                 install -d "$d" || e=$?
225                         else
226                                 mkdir "$d" || e=$?
227                         fi
228                 fi
229         done
230         return $e
231 }
232
233 list_resolv()
234 {
235         [ -d "$IFACEDIR" ] || return 0
236
237         local report=false list= retval=0 cmd="$1"
238         shift
239
240         # If we have an interface ordering list, then use that.
241         # It works by just using pathname expansion in the interface directory.
242         if [ -n "$1" ]; then
243                 list="$*"
244                 $force || report=true
245         else
246                 cd "$IFACEDIR"
247                 for i in $interface_order; do
248                         [ -e "$i" ] && list="$list $i"
249                         for ii in "$i":*; do
250                                 [ -e "$ii" ] && list="$list $ii"
251                         done
252                 done
253                 for i in $dynamic_order; do
254                         if [ -e "$i" -a ! -e "$METRICDIR/"*" $i" ]; then
255                                 list="$list $i"
256                         fi
257                         for ii in "$i":*; do
258                                 if [ -e "$ii" -a ! -e "$METRICDIR/"*" $ii" ]; then
259                                         list="$list $ii"
260                                 fi
261                         done
262                 done
263                 if [ -d "$METRICDIR" ]; then
264                         cd "$METRICDIR"
265                         for i in *; do
266                                 list="$list ${i#* }"
267                         done
268                 fi
269                 list="$list *"
270         fi
271
272         cd "$IFACEDIR"
273         for i in $(uniqify $list); do
274                 # Only list interfaces which we really have
275                 if ! [ -e "$i" ]; then
276                         if $report; then
277                                 echo "No resolv.conf for interface $i" >&2
278                                 retval=$(($retval + 1))
279                         fi
280                         continue
281                 fi
282                 
283                 if [ "$cmd" = i -o "$cmd" = "-i" ]; then
284                         printf %s "$i "
285                 else
286                         echo_resolv "$i"
287                 fi
288         done
289         [ "$cmd" = i -o "$cmd" = "-i" ] && echo
290         return $retval
291 }
292
293 list_remove() {
294         local list= e= l= result= found= retval=0
295
296         [ -z "$2" ] && return 0
297         eval list=\"\$$1\"
298         shift
299
300         set -f
301         for e; do
302                 found=false
303                 for l in $list; do
304                         case "$e" in
305                         $l) found=true;;
306                         esac
307                         $found && break
308                 done
309                 if $found; then
310                         retval=$(($retval + 1))
311                 else
312                         result="$result $e"
313                 fi
314         done
315         set +f
316         echo "${result# *}"
317         return $retval
318 }
319
320 echo_prepend()
321 {
322         echo "# Generated by resolvconf"
323         if [ -n "$search_domains" ]; then
324                 echo "search $search_domains"
325         fi
326         for n in $name_servers; do
327                 echo "nameserver $n"
328         done
329         echo
330 }
331
332 echo_append()
333 {
334         echo "# Generated by resolvconf"
335         if [ -n "$search_domains_append" ]; then
336                 echo "search $search_domains_append"
337         fi
338         for n in $name_servers_append; do
339                 echo "nameserver $n"
340         done
341         echo
342 }
343
344 make_vars()
345 {
346         local newdomains= d= dn= newns= ns=
347
348         # Clear variables
349         DOMAIN=
350         DOMAINS=
351         SEARCH=
352         NAMESERVERS=
353         LOCALNAMESERVERS=
354
355         if [ -n "$name_servers" -o -n "$search_domains" ]; then
356                 eval "$(echo_prepend | parse_resolv)"
357         fi
358         eval "$(list_resolv -l "$@" | parse_resolv)"
359         if [ -n "$name_servers_append" -o -n "$search_domains_append" ]; then
360                 eval "$(echo_append | parse_resolv)"
361         fi
362
363         # Ensure that we only list each domain once
364         for d in $DOMAINS; do
365                 dn="${d%%:*}"
366                 list_remove domain_blacklist "$dn" >/dev/null || continue
367                 case " $newdomains" in
368                 *" ${dn}:"*) continue;;
369                 esac
370                 newns=
371                 for nd in $DOMAINS; do
372                         if [ "$dn" = "${nd%%:*}" ]; then
373                                 ns="${nd#*:}"
374                                 while [ -n "$ns" ]; do
375                                         case ",$newns," in
376                                         *,${ns%%,*},*) ;;
377                                         *) list_remove name_server_blacklist \
378                                                 "$ns" >/dev/null \
379                                         && newns="$newns${newns:+,}${ns%%,*}";;
380                                         esac
381                                         [ "$ns" = "${ns#*,}" ] && break
382                                         ns="${ns#*,}"
383                                 done
384                         fi
385                 done
386                 if [ -n "$newns" ]; then
387                         newdomains="$newdomains${newdomains:+ }$dn:$newns"
388                 fi
389         done
390         DOMAIN="$(list_remove domain_blacklist $DOMAIN)"
391         SEARCH="$(uniqify $SEARCH)"
392         SEARCH="$(list_remove domain_blacklist $SEARCH)"
393         NAMESERVERS="$(uniqify $NAMESERVERS)"
394         NAMESERVERS="$(list_remove name_server_blacklist $NAMESERVERS)"
395         LOCALNAMESERVERS="$(uniqify $LOCALNAMESERVERS)"
396         LOCALNAMESERVERS="$(list_remove name_server_blacklist $LOCALNAMESERVERS)"
397         echo "DOMAIN='$DOMAIN'"
398         echo "SEARCH='$SEARCH'"
399         echo "NAMESERVERS='$NAMESERVERS'"
400         echo "LOCALNAMESERVERS='$LOCALNAMESERVERS'"
401         echo "DOMAINS='$newdomains'"
402 }
403
404 force=false
405 while getopts a:Dd:fhIilm:puv OPT; do
406         case "$OPT" in
407         f) force=true;;
408         h) usage;;
409         m) IF_METRIC="$OPTARG";;
410         p) IF_PRIVATE=1;;
411         '?') ;;
412         *) cmd="$OPT"; iface="$OPTARG";;
413         esac
414 done
415 shift $(($OPTIND - 1))
416 args="$iface${iface:+ }$*"
417
418 # -I inits the state dir
419 if [ "$cmd" = I ]; then
420         if [ -d "$VARDIR" ]; then
421                 rm -rf "$VARDIR"/*
422         fi
423         exit $?
424 fi
425
426 # -D ensures that the listed config file base dirs exist
427 if [ "$cmd" = D ]; then
428         config_mkdirs "$@"
429         exit $?
430 fi
431
432 # -l lists our resolv files, optionally for a specific interface
433 if [ "$cmd" = l -o "$cmd" = i ]; then
434         list_resolv "$cmd" "$args"
435         exit $?
436 fi
437
438 # Not normally needed, but subscribers should be able to run independently
439 if [ "$cmd" = v ]; then
440         make_vars "$iface"
441         exit $?
442 fi
443
444 # Test that we have valid options
445 if [ "$cmd" = a -o "$cmd" = d ]; then
446         if [ -z "$iface" ]; then
447                 usage "Interface not specified"
448         fi
449 elif [ "$cmd" != u ]; then
450         [ -n "$cmd" -a "$cmd" != h ] && usage "Unknown option $cmd"
451         usage
452 fi
453
454 if [ "$cmd" = a ]; then
455         for x in '/' \\ ' ' '*'; do
456                 case "$iface" in
457                 *[$x]*) error_exit "$x not allowed in interface name";;
458                 esac
459         done
460         for x in '.' '-' '~'; do
461                 case "$iface" in
462                 [$x]*) error_exit \
463                         "$x not allowed at start of interface name";;
464                 esac
465         done
466         [ "$cmd" = a -a -t 0 ] && error_exit "No file given via stdin"
467 fi
468
469 if [ ! -d "$VARDIR" ]; then
470         if [ -L "$VARDIR" ]; then
471                 dir="$(readlink "$VARDIR")"
472                 # link maybe relative
473                 cd "${VARDIR%/*}"
474                 if ! mkdir -m 0755 -p "$dir"; then
475                         error_exit "Failed to create needed" \
476                                 "directory $dir"
477                 fi
478         else
479                 if ! mkdir -m 0755 -p "$VARDIR"; then
480                         error_exit "Failed to create needed" \
481                                 "directory $VARDIR"
482                 fi
483         fi
484 fi
485
486 if [ ! -d "$IFACEDIR" ]; then
487         mkdir -m 0755 -p "$IFACEDIR" || \
488                 error_exit "Failed to create needed directory $IFACEDIR"
489 else
490         # Delete any existing information about the interface
491         if [ "$cmd" = d ]; then
492                 cd "$IFACEDIR"
493                 for i in $args; do
494                         if [ "$cmd" = d -a ! -e "$i" ]; then
495                                 $force && continue
496                                 error_exit "No resolv.conf for" \
497                                         "interface $i"
498                         fi
499                         rm -f "$i" "$METRICDIR/"*" $i" \
500                                 "$PRIVATEDIR/$i" || exit $?
501                 done
502         fi
503 fi
504
505 if [ "$cmd" = a ]; then
506         # Read resolv.conf from stdin
507         resolv="$(cat)"
508         changed=false
509         changedfile=false
510         # If what we are given matches what we have, then do nothing
511         if [ -e "$IFACEDIR/$iface" ]; then
512                 if [ "$(echo "$resolv")" != \
513                         "$(cat "$IFACEDIR/$iface")" ]
514                 then
515                         changed=true
516                         changedfile=true
517                 fi
518         else
519                 changed=true
520                 changedfile=true
521         fi
522         # Set metric and private before creating the interface resolv.conf file
523         # to ensure that it will have the correct flags
524         [ ! -d "$METRICDIR" ] && mkdir "$METRICDIR"
525         oldmetric="$METRICDIR/"*" $iface"
526         newmetric=
527         if [ -n "$IF_METRIC" ]; then
528                 # Pad metric to 6 characters, so 5 is less than 10
529                 while [ ${#IF_METRIC} -le 6 ]; do
530                         IF_METRIC="0$IF_METRIC"
531                 done
532                 newmetric="$METRICDIR/$IF_METRIC $iface"
533         fi
534         rm -f "$METRICDIR/"*" $iface"
535         [ "$oldmetric" != "$newmetric" -a \
536             "$oldmetric" != "$METRICDIR/* $iface" ] &&
537                 changed=true
538         [ -n "$newmetric" ] && echo " " >"$newmetric"
539         case "$IF_PRIVATE" in
540         [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
541                 if [ ! -d "$PRIVATEDIR" ]; then
542                         [ -e "$PRIVATEDIR" ] && rm "$PRIVATEDIR"
543                         mkdir "$PRIVATEDIR"
544                 fi
545                 [ -e "$PRIVATEDIR/$iface" ] || changed=true
546                 [ -d "$PRIVATEDIR" ] && echo " " >"$PRIVATEDIR/$iface"
547                 ;;
548         *)
549                 if [ -e "$PRIVATEDIR/$iface" ]; then
550                         rm -f "$PRIVATEDIR/$iface"
551                         changed=true
552                 fi
553                 ;;
554         esac
555         if $changedfile; then
556                 # Ensure that creating the file is an atomic operation
557                 if [ ! -d "$TMPDIR" ]; then
558                         mkdir -m 0755 -p "$TMPDIR" || \
559                             error_exit \
560                                 "Failed to create needed directory $TMPDIR"
561                 fi
562                 TMPFILE="$TMPDIR/$iface.$$"
563                 cleanup() { [ -n "$TMPFILE" ] && rm -f "$TMPFILE"; }
564                 trap cleanup EXIT
565                 echo "$resolv" >"$TMPFILE" || exit $?
566                 mv -f "$TMPFILE" "$IFACEDIR/$iface" || exit $?
567                 TMPFILE=
568         fi
569         $changed || exit 0
570         unset changed oldmetric newmetric
571 fi
572
573 eval "$(make_vars)"
574 export RESOLVCONF DOMAINS SEARCH NAMESERVERS LOCALNAMESERVERS
575 : ${list_resolv:=list_resolv -l}
576 retval=0
577 for script in "$LIBEXECDIR"/*; do
578         if [ -f "$script" ]; then
579                 if [ -x "$script" ]; then
580                         "$script" "$cmd" "$iface"
581                 else
582                         (set -- "$cmd" "$iface"; . "$script")
583                 fi
584                 retval=$(($retval + $?))
585         fi
586 done
587 exit $retval