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