Don't moan if dnsmasq has not written a pidfile.
[openresolv] / resolvconf.in
1 #!/bin/sh
2 # Copyright (c) 2007-2014 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 LOCKDIR="$VARDIR/lock"
51
52 : ${dynamic_order:=tap[0-9]* tun[0-9]* vpn vpn[0-9]* ppp[0-9]* ippp[0-9]*}
53 : ${interface_order:=lo lo[0-9]*}
54 : ${name_server_blacklist:=0.0.0.0}
55
56 warn()
57 {
58         echo "$*" >&2
59 }
60
61 error_exit()
62 {
63         echo "$*" >&2
64         exit 1
65 }
66
67 usage()
68 {
69         cat <<-EOF
70         Usage: ${RESOLVCONF##*/} [options]
71
72         Inform the system about any DNS updates.
73
74         Options:
75           -a \$INTERFACE    Add DNS information to the specified interface
76                            (DNS supplied via stdin in resolv.conf format)
77           -m metric        Give the added DNS information a metric
78           -p               Mark the interface as private
79           -d \$INTERFACE    Delete DNS information from the specified interface
80           -f               Ignore non existant interfaces
81           -I               Init the state dir
82           -u               Run updates from our current DNS information
83           -l [\$PATTERN]    Show DNS information, optionally from interfaces
84                            that match the specified pattern
85           -i [\$PATTERN]    Show interfaces that have supplied DNS information
86                    optionally from interfaces that match the specified
87                    pattern
88           -v [\$PATTERN]    echo NEWDOMAIN, NEWSEARCH and NEWNS variables to
89                            the console
90           -h               Show this help cruft
91         EOF
92         [ -z "$1" ] && exit 0
93         echo
94         error_exit "$*"
95 }
96
97 echo_resolv()
98 {
99         local line= OIFS="$IFS"
100
101         [ -n "$1" -a -e "$IFACEDIR/$1" ] || return 1
102         echo "# resolv.conf from $1"
103         # Our variable maker works of the fact each resolv.conf per interface
104         # is separated by blank lines.
105         # So we remove them when echoing them.
106         while read -r line; do
107                 IFS="$OIFS"
108                 if [ -n "$line" ]; then
109                         # We need to set IFS here to preserve any whitespace
110                         IFS=''
111                         printf "%s\n" "$line"
112                 fi
113         done < "$IFACEDIR/$1"
114         echo
115         IFS="$OIFS"
116 }
117
118 # Parse resolv.conf's and make variables
119 # for domain name servers, search name servers and global nameservers
120 parse_resolv()
121 {
122         local line= ns= ds= search= d= n= newns=
123         local new=true iface= private=false p= domain=
124
125         newns=
126
127         while read -r line; do
128                 case "$line" in
129                 "# resolv.conf from "*)
130                         if ${new}; then
131                                 iface="${line#\# resolv.conf from *}"
132                                 new=false
133                                 if [ -e "$PRIVATEDIR/$iface" ]; then
134                                         private=true
135                                 else
136                                         # Allow expansion
137                                         cd "$IFACEDIR"
138                                         private=false
139                                         for p in $private_interfaces; do
140                                                 case "$iface" in
141                                                 "$p"|"$p":*) private=true; break;;
142                                                 esac
143                                         done
144                                 fi
145                         fi
146                         ;;
147                 "nameserver "*)
148                         case "${line#* }" in
149                         127.*|0.0.0.0|255.255.255.255|::1)
150                                 echo "LOCALNAMESERVERS=\"\$LOCALNAMESERVERS ${line#* }\""
151                                 continue
152                                 ;;
153                         esac
154                         ns="$ns${line#* } "
155                         ;;
156                 "domain "*)
157                         if [ -z "$domain" ]; then
158                                 domain="${line#* }"
159                                 echo "DOMAIN=\"$domain\""
160                         fi
161                         search="${line#* }"
162                         ;;
163                 "search "*)
164                         search="${line#* }"
165                         ;;
166                 *)
167                         [ -n "$line" ] && continue
168                         if [ -n "$ns" -a -n "$search" ]; then
169                                 newns=
170                                 for n in $ns; do
171                                         newns="$newns${newns:+,}$n"
172                                 done
173                                 ds=
174                                 for d in $search; do
175                                         ds="$ds${ds:+ }$d:$newns"
176                                 done
177                                 echo "DOMAINS=\"\$DOMAINS $ds\""
178                         fi
179                         echo "SEARCH=\"\$SEARCH $search\""
180                         if ! $private; then
181                                 echo "NAMESERVERS=\"\$NAMESERVERS $ns\""
182                         fi
183                         ns=
184                         search=
185                         new=true
186                         ;;
187                 esac
188         done
189 }
190
191 uniqify()
192 {
193         local result=
194         while [ -n "$1" ]; do
195                 case " $result " in
196                 *" $1 "*);;
197                 *) result="$result $1";;
198                 esac
199                 shift
200         done
201         echo "${result# *}"
202 }
203
204 dirname()
205 {
206         local dir= OIFS="$IFS"
207         local IFS=/
208         set -- $@
209         IFS="$OIFS"
210         if [ -n "$1" ]; then
211                 printf %s .
212         else
213                 shift
214         fi
215         while [ -n "$2" ]; do
216                 printf "/%s" "$1"
217                 shift
218         done
219         printf "\n"
220 }
221
222 config_mkdirs()
223 {
224         local e=0 f d
225         for f; do
226                 [ -n "$f" ] || continue
227                 d="$(dirname "$f")"
228                 if [ ! -d "$d" ]; then
229                         if type install >/dev/null 2>&1; then
230                                 install -d "$d" || e=$?
231                         else
232                                 mkdir "$d" || e=$?
233                         fi
234                 fi
235         done
236         return $e
237 }
238
239 list_resolv()
240 {
241         [ -d "$IFACEDIR" ] || return 0
242
243         local report=false list= retval=0 cmd="$1"
244         shift
245
246         # If we have an interface ordering list, then use that.
247         # It works by just using pathname expansion in the interface directory.
248         if [ -n "$1" ]; then
249                 list="$*"
250                 $force || report=true
251         else
252                 cd "$IFACEDIR"
253                 for i in $interface_order; do
254                         [ -e "$i" ] && list="$list $i"
255                         for ii in "$i":*; do
256                                 [ -e "$ii" ] && list="$list $ii"
257                         done
258                 done
259                 for i in $dynamic_order; do
260                         if [ -e "$i" -a ! -e "$METRICDIR/"*" $i" ]; then
261                                 list="$list $i"
262                         fi
263                         for ii in "$i":*; do
264                                 if [ -e "$ii" -a ! -e "$METRICDIR/"*" $ii" ]; then
265                                         list="$list $ii"
266                                 fi
267                         done
268                 done
269                 if [ -d "$METRICDIR" ]; then
270                         cd "$METRICDIR"
271                         for i in *; do
272                                 list="$list ${i#* }"
273                         done
274                 fi
275                 list="$list *"
276         fi
277
278         cd "$IFACEDIR"
279         for i in $(uniqify $list); do
280                 # Only list interfaces which we really have
281                 if ! [ -e "$i" ]; then
282                         if $report; then
283                                 echo "No resolv.conf for interface $i" >&2
284                                 retval=$(($retval + 1))
285                         fi
286                         continue
287                 fi
288                 
289                 if [ "$cmd" = i -o "$cmd" = "-i" ]; then
290                         printf %s "$i "
291                 else
292                         echo_resolv "$i"
293                 fi
294         done
295         [ "$cmd" = i -o "$cmd" = "-i" ] && echo
296         return $retval
297 }
298
299 list_remove() {
300         local list= e= l= result= found= retval=0
301
302         [ -z "$2" ] && return 0
303         eval list=\"\$$1\"
304         shift
305
306         set -f
307         for e; do
308                 found=false
309                 for l in $list; do
310                         case "$e" in
311                         $l) found=true;;
312                         esac
313                         $found && break
314                 done
315                 if $found; then
316                         retval=$(($retval + 1))
317                 else
318                         result="$result $e"
319                 fi
320         done
321         set +f
322         echo "${result# *}"
323         return $retval
324 }
325
326 echo_prepend()
327 {
328         echo "# Generated by resolvconf"
329         if [ -n "$search_domains" ]; then
330                 echo "search $search_domains"
331         fi
332         for n in $name_servers; do
333                 echo "nameserver $n"
334         done
335         echo
336 }
337
338 echo_append()
339 {
340         echo "# Generated by resolvconf"
341         if [ -n "$search_domains_append" ]; then
342                 echo "search $search_domains_append"
343         fi
344         for n in $name_servers_append; do
345                 echo "nameserver $n"
346         done
347         echo
348 }
349
350 make_vars()
351 {
352         local newdomains= d= dn= newns= ns=
353
354         # Clear variables
355         DOMAIN=
356         DOMAINS=
357         SEARCH=
358         NAMESERVERS=
359         LOCALNAMESERVERS=
360
361         if [ -n "$name_servers" -o -n "$search_domains" ]; then
362                 eval "$(echo_prepend | parse_resolv)"
363         fi
364         eval "$(list_resolv -l "$@" | parse_resolv)"
365         if [ -n "$name_servers_append" -o -n "$search_domains_append" ]; then
366                 eval "$(echo_append | parse_resolv)"
367         fi
368
369         # Ensure that we only list each domain once
370         for d in $DOMAINS; do
371                 dn="${d%%:*}"
372                 list_remove domain_blacklist "$dn" >/dev/null || continue
373                 case " $newdomains" in
374                 *" ${dn}:"*) continue;;
375                 esac
376                 newns=
377                 for nd in $DOMAINS; do
378                         if [ "$dn" = "${nd%%:*}" ]; then
379                                 ns="${nd#*:}"
380                                 while [ -n "$ns" ]; do
381                                         case ",$newns," in
382                                         *,${ns%%,*},*) ;;
383                                         *) list_remove name_server_blacklist \
384                                                 "$ns" >/dev/null \
385                                         && newns="$newns${newns:+,}${ns%%,*}";;
386                                         esac
387                                         [ "$ns" = "${ns#*,}" ] && break
388                                         ns="${ns#*,}"
389                                 done
390                         fi
391                 done
392                 if [ -n "$newns" ]; then
393                         newdomains="$newdomains${newdomains:+ }$dn:$newns"
394                 fi
395         done
396         DOMAIN="$(list_remove domain_blacklist $DOMAIN)"
397         SEARCH="$(uniqify $SEARCH)"
398         SEARCH="$(list_remove domain_blacklist $SEARCH)"
399         NAMESERVERS="$(uniqify $NAMESERVERS)"
400         NAMESERVERS="$(list_remove name_server_blacklist $NAMESERVERS)"
401         LOCALNAMESERVERS="$(uniqify $LOCALNAMESERVERS)"
402         LOCALNAMESERVERS="$(list_remove name_server_blacklist $LOCALNAMESERVERS)"
403         echo "DOMAIN='$DOMAIN'"
404         echo "SEARCH='$SEARCH'"
405         echo "NAMESERVERS='$NAMESERVERS'"
406         echo "LOCALNAMESERVERS='$LOCALNAMESERVERS'"
407         echo "DOMAINS='$newdomains'"
408 }
409
410 force=false
411 while getopts a:Dd:fhIilm:puv OPT; do
412         case "$OPT" in
413         f) force=true;;
414         h) usage;;
415         m) IF_METRIC="$OPTARG";;
416         p) IF_PRIVATE=1;;
417         '?') ;;
418         *) cmd="$OPT"; iface="$OPTARG";;
419         esac
420 done
421 shift $(($OPTIND - 1))
422 args="$iface${iface:+ }$*"
423
424 # -I inits the state dir
425 if [ "$cmd" = I ]; then
426         if [ -d "$VARDIR" ]; then
427                 rm -rf "$VARDIR"/*
428         fi
429         exit $?
430 fi
431
432 # -D ensures that the listed config file base dirs exist
433 if [ "$cmd" = D ]; then
434         config_mkdirs "$@"
435         exit $?
436 fi
437
438 # -l lists our resolv files, optionally for a specific interface
439 if [ "$cmd" = l -o "$cmd" = i ]; then
440         list_resolv "$cmd" "$args"
441         exit $?
442 fi
443
444 # Not normally needed, but subscribers should be able to run independently
445 if [ "$cmd" = v ]; then
446         make_vars "$iface"
447         exit $?
448 fi
449
450 # Test that we have valid options
451 if [ "$cmd" = a -o "$cmd" = d ]; then
452         if [ -z "$iface" ]; then
453                 usage "Interface not specified"
454         fi
455 elif [ "$cmd" != u ]; then
456         [ -n "$cmd" -a "$cmd" != h ] && usage "Unknown option $cmd"
457         usage
458 fi
459
460 if [ "$cmd" = a ]; then
461         for x in '/' \\ ' ' '*'; do
462                 case "$iface" in
463                 *[$x]*) error_exit "$x not allowed in interface name";;
464                 esac
465         done
466         for x in '.' '-' '~'; do
467                 case "$iface" in
468                 [$x]*) error_exit \
469                         "$x not allowed at start of interface name";;
470                 esac
471         done
472         [ "$cmd" = a -a -t 0 ] && error_exit "No file given via stdin"
473 fi
474
475 if [ ! -d "$VARDIR" ]; then
476         if [ -L "$VARDIR" ]; then
477                 dir="$(readlink "$VARDIR")"
478                 # link maybe relative
479                 cd "${VARDIR%/*}"
480                 if ! mkdir -m 0755 -p "$dir"; then
481                         error_exit "Failed to create needed" \
482                                 "directory $dir"
483                 fi
484         else
485                 if ! mkdir -m 0755 -p "$VARDIR"; then
486                         error_exit "Failed to create needed" \
487                                 "directory $VARDIR"
488                 fi
489         fi
490 fi
491
492 if [ ! -d "$IFACEDIR" ]; then
493         mkdir -m 0755 -p "$IFACEDIR" || \
494                 error_exit "Failed to create needed directory $IFACEDIR"
495 else
496         # Delete any existing information about the interface
497         if [ "$cmd" = d ]; then
498                 cd "$IFACEDIR"
499                 changed=false
500                 for i in $args; do
501                         if [ -e "$i" ]; then
502                                 changed=true
503                         elif ! ${force}; then
504                                 warn "No resolv.conf for interface $i"
505                         fi
506                         rm -f "$i" "$METRICDIR/"*" $i" \
507                                 "$PRIVATEDIR/$i" || exit $?
508                 done
509                 if ! ${changed}; then
510                         # Set the return code based on the forced flag
511                         ${force}
512                         exit $?
513                 fi
514         fi
515 fi
516
517 if [ "$cmd" = a ]; then
518         # Read resolv.conf from stdin
519         resolv="$(cat)"
520         changed=false
521         changedfile=false
522         # If what we are given matches what we have, then do nothing
523         if [ -e "$IFACEDIR/$iface" ]; then
524                 if [ "$(echo "$resolv")" != \
525                         "$(cat "$IFACEDIR/$iface")" ]
526                 then
527                         changed=true
528                         changedfile=true
529                 fi
530         else
531                 changed=true
532                 changedfile=true
533         fi
534         # Set metric and private before creating the interface resolv.conf file
535         # to ensure that it will have the correct flags
536         [ ! -d "$METRICDIR" ] && mkdir "$METRICDIR"
537         oldmetric="$METRICDIR/"*" $iface"
538         newmetric=
539         if [ -n "$IF_METRIC" ]; then
540                 # Pad metric to 6 characters, so 5 is less than 10
541                 while [ ${#IF_METRIC} -le 6 ]; do
542                         IF_METRIC="0$IF_METRIC"
543                 done
544                 newmetric="$METRICDIR/$IF_METRIC $iface"
545         fi
546         rm -f "$METRICDIR/"*" $iface"
547         [ "$oldmetric" != "$newmetric" -a \
548             "$oldmetric" != "$METRICDIR/* $iface" ] &&
549                 changed=true
550         [ -n "$newmetric" ] && echo " " >"$newmetric"
551         case "$IF_PRIVATE" in
552         [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
553                 if [ ! -d "$PRIVATEDIR" ]; then
554                         [ -e "$PRIVATEDIR" ] && rm "$PRIVATEDIR"
555                         mkdir "$PRIVATEDIR"
556                 fi
557                 [ -e "$PRIVATEDIR/$iface" ] || changed=true
558                 [ -d "$PRIVATEDIR" ] && echo " " >"$PRIVATEDIR/$iface"
559                 ;;
560         *)
561                 if [ -e "$PRIVATEDIR/$iface" ]; then
562                         rm -f "$PRIVATEDIR/$iface"
563                         changed=true
564                 fi
565                 ;;
566         esac
567         if $changedfile; then
568                 # Ensure that creating the file is an atomic operation
569                 if [ ! -d "$TMPDIR" ]; then
570                         mkdir -m 0755 -p "$TMPDIR" || \
571                             error_exit \
572                                 "Failed to create needed directory $TMPDIR"
573                 fi
574                 TMPFILE="$TMPDIR/$iface.$$"
575                 cleanup() { [ -n "$TMPFILE" ] && rm -f "$TMPFILE"; }
576                 trap cleanup EXIT
577                 echo "$resolv" >"$TMPFILE" || exit $?
578                 mv -f "$TMPFILE" "$IFACEDIR/$iface" || exit $?
579                 TMPFILE=
580         fi
581         $changed || exit 0
582         unset changed oldmetric newmetric
583 fi
584
585 # An interface was added, deleted or changed.
586 # These above actions are atomic, however calling our subcribers is not.
587 # Even if we do our very best, the action of restarting the subscriber daemon
588 # is not guaranteed to be serialised due to our many flavours of OS we support.
589 # As such we spinlock at this point as best we can.
590 # We don't use flock(1) because it's not widely available and normally resides
591 # in /usr which we do our very best to operate without.
592 : ${lock_timeout:=10}
593 while true; do
594         if mkdir "$LOCKDIR" 2>/dev/null; then
595                 trap 'rm -rf "$LOCKDIR";' EXIT
596                 trap 'rm -rf "$LOCKDIR"; exit 1' INT QUIT ABRT SEGV ALRM TERM
597                 echo $$ >"$LOCKDIR/pid"
598                 break
599         fi
600         lock_timeout=$(($lock_timeout - 1))
601         if [ "$lock_timeout" -le 0 ]; then
602                 pid=$(cat "$LOCKDIR/pid")
603                 error_exit "timed out waiting for lock from pid $pid"
604         fi
605         sleep 1
606 done
607
608 eval "$(make_vars)"
609 export RESOLVCONF DOMAINS SEARCH NAMESERVERS LOCALNAMESERVERS
610 : ${list_resolv:=list_resolv -l}
611 retval=0
612 for script in "$LIBEXECDIR"/*; do
613         if [ -f "$script" ]; then
614                 if [ -x "$script" ]; then
615                         "$script" "$cmd" "$iface"
616                 else
617                         (set -- "$cmd" "$iface"; . "$script")
618                 fi
619                 retval=$(($retval + $?))
620         fi
621 done
622 exit $retval