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