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