Allow expansion of private_interfaces
[openresolv] / resolvconf.in
1 #!/bin/sh
2 # Copyright (c) 2007-2009 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 # Support original resolvconf configuration layout
32 # as well as the openresolv config file
33 if [ -f "$SYSCONFDIR"/resolvconf.conf ]; then
34         . "$SYSCONFDIR"/resolvconf.conf
35         [ -n "$state_dir" ] && VARDIR="$state_dir"
36 elif [ -d "$SYSCONFDIR/resolvconf" ]; then
37         SYSCONFDIR="$SYSCONFDIR/resolvconf"
38         if [ -f "$SYSCONFDIR"/interface-order ]; then
39                 interface_order="$(cat "$SYSCONFDIR"/interface-order)"
40         fi
41 fi
42 IFACEDIR="$VARDIR/interfaces"
43 METRICDIR="$VARDIR/metrics"
44 PRIVATEDIR="$VARDIR/private"
45
46 : ${dynamic_order:=tap[0-9]* tun[0-9]* vpn vpn[0-9]* ppp[0-9]* ippp[0-9]*}
47 : ${interface_order:=lo lo[0-9]*}
48
49 error_exit()
50 {
51         echo "$*" >&2
52         exit 1
53 }
54
55 usage()
56 {
57         cat <<-EOF
58         Usage: ${RESOLVCONF##*/} [options]
59
60         Inform the system about any DNS updates.
61
62         Options:
63           -a \$INTERFACE    Add DNS information to the specified interface
64                            (DNS supplied via stdin in resolv.conf format)
65           -m metric        Give the added DNS information a metric
66           -p               Mark the interface as private
67           -d \$INTERFACE    Delete DNS information from the specified interface
68           -f               Ignore non existant interfaces
69           -I               Init the state dir
70           -u               Run updates from our current DNS information
71           -l [\$PATTERN]    Show DNS information, optionally from interfaces
72                            that match the specified pattern
73           -i [\$PATTERN]    Show interfaces that have supplied DNS information
74                    optionally from interfaces that match the specified
75                    pattern
76           -v [\$PATTERN]    echo NEWDOMAIN, NEWSEARCH and NEWNS variables to
77                            the console
78           -h               Show this help cruft
79         EOF
80         [ -z "$1" ] && exit 0
81         echo
82         error_exit "$*"
83 }
84
85 echo_resolv()
86 {
87         local line=
88         [ -n "$1" -a -e "$IFACEDIR/$1" ] || return 1
89         echo "# resolv.conf from $1"
90         # Our variable maker works of the fact each resolv.conf per interface
91         # is separated by blank lines.
92         # So we remove them when echoing them.
93         while read line; do
94                 [ -n "$line" ] && echo "$line"
95         done < "$IFACEDIR/$1"
96         echo
97 }
98
99 # Parse resolv.conf's and make variables
100 # for domain name servers, search name servers and global nameservers
101 parse_resolv()
102 {
103         local line= ns= ds= search= d= n= newns=
104         local new=true iface= private=false p=
105
106         echo "DOMAINS="
107         echo "SEARCH="
108         echo "NAMESERVERS="
109
110         while read line; do
111                 case "$line" in
112                 "# resolv.conf from "*)
113                         if ${new}; then
114                                 iface="${line#\# resolv.conf from *}"
115                                 new=false
116                                 if [ -e "$PRIVATEDIR/$iface" ]; then
117                                         private=true
118                                 else
119                                         # Allow expansion
120                                         cd "$IFACEDIR"
121                                         private=false
122                                         for p in $private_interfaces; do
123                                                 if [ "$p" = "$iface" ]; then
124                                                         private=true
125                                                         break
126                                                 fi
127                                         done
128                                 fi
129                         fi
130                         ;;
131                 "nameserver "*)
132                         case "${line#* }" in
133                         127.*|0.0.0.0|255.255.255.255) continue;;
134                         esac
135                         ns="$ns${line#* } "
136                         ;;
137                 "domain "*|"search "*)
138                         search="${line#* }"
139                         ;;
140                 *)
141                         [ -n "$line" ] && continue
142                         if [ -n "$ns" -a -n "$search" ]; then
143                                 newns=
144                                 for n in $ns; do
145                                         newns="$newns${newns:+,}$n"
146                                 done
147                                 ds=
148                                 for d in $search; do
149                                         ds="$ds${ds:+ }$d:$newns"
150                                 done
151                                 echo "DOMAINS=\"\$DOMAINS $ds\""
152                         fi
153                         echo "SEARCH=\"\$SEARCH $search\""
154                         if ! $private; then
155                                 echo "NAMESERVERS=\"\$NAMESERVERS $ns\""
156                         fi
157                         ns=
158                         search=
159                         new=true
160                         ;;
161                 esac
162         done
163 }
164
165 uniqify()
166 {
167         local result=
168         while [ -n "$1" ]; do
169                 case " $result " in
170                 *" $1 "*);;
171                 *) result="$result $1";;
172                 esac
173                 shift
174         done
175         echo "${result# *}"
176 }
177
178 list_resolv()
179 {
180         [ -d "$IFACEDIR" ] || return 0
181
182         local report=false list= retval=0 cmd="$1"
183         shift
184
185         # If we have an interface ordering list, then use that.
186         # It works by just using pathname expansion in the interface directory.
187         if [ -n "$1" ]; then
188                 list="$@"
189                 $force || report=true
190         else
191                 cd "$IFACEDIR"
192                 for i in $interface_order; do
193                         [ -e "$i" ] && list="$list $i"
194                 done
195                 for i in $dynamic_order; do
196                         if [ -e "$i" -a ! -e "$METRICDIR/"*" $i" ]; then
197                                 list="$list $i"
198                         fi
199                 done
200                 if [ -d "$METRICDIR" ]; then
201                         cd "$METRICDIR"
202                         for i in *; do
203                                 list="$list ${i#* }"
204                         done
205                 fi
206                 list="$list *"
207         fi
208
209         cd "$IFACEDIR"
210         for i in $(uniqify $list); do
211                 # Only list interfaces which we really have
212                 if ! [ -e "$i" ]; then
213                         if $report; then
214                                 echo "No resolv.conf for interface $i" >&2
215                                 retval=$(($retval + 1))
216                         fi
217                         continue
218                 fi
219                 
220                 if [ "$cmd" = i -o "$cmd" = "-i" ]; then
221                         printf "$i "
222                 else
223                         echo_resolv "$i"
224                 fi
225         done
226         [ "$cmd" = i -o "$cmd" = "-i" ] && echo
227         return $retval
228 }
229
230 make_vars()
231 {
232         eval "$(list_resolv -l "$@" | parse_resolv)"
233
234         # Ensure that we only list each domain once
235         newdomains=
236         for d in $DOMAINS; do
237                 dn="${d%%:*}"
238                 case " $newdomains" in
239                 *" ${dn}:"*) continue;;
240                 esac
241                 newdomains="$newdomains${newdomains:+ }$dn:"
242                 newns=
243                 for nd in $DOMAINS; do
244                         if [ "$dn" = "${nd%%:*}" ]; then
245                                 ns="${nd#*:}"
246                                 while [ -n "$ns" ]; do
247                                         case ",$newns," in
248                                         *,${ns%%,*},*) ;;
249                                         *) newns="$newns${newns:+,}${ns%%,*}";;
250                                         esac
251                                         [ "$ns" = "${ns#*,}" ] && break
252                                         ns="${ns#*,}"
253                                 done
254                         fi
255                 done
256                 newdomains="$newdomains$newns"
257         done
258         echo "DOMAINS='$newdomains'"
259         echo "SEARCH='$(uniqify $SEARCH)'"
260         echo "NAMESERVERS='$(uniqify $NAMESERVERS)'"
261 }
262
263 force=false
264 while getopts a:d:fhilm:puv OPT; do
265         case "$OPT" in
266         f) force=true;;
267         h) usage;;
268         m) IF_METRIC="$OPTARG";;
269         p) IF_PRIVATE=1;;
270         '?') ;;
271         *) cmd="$OPT"; iface="$OPTARG";;
272         esac
273 done
274 shift $(($OPTIND - 1))
275 args="$iface${iface:+ }$@"
276
277 # -I inits the state dir
278 if [ "$cmd" = I ]; then
279         if [ -d "$VARDIR" ]; then
280                 rm -rf "$VARDIR"/*
281         fi
282         exit $?
283 fi
284
285 # -l lists our resolv files, optionally for a specific interface
286 if [ "$cmd" = l -o "$cmd" = i ]; then
287         list_resolv "$cmd" "$args"
288         exit $?
289 fi
290
291 # Not normally needed, but subscribers should be able to run independently
292 if [ "$cmd" = v ]; then
293         make_vars "$iface"
294         exit $?
295 fi
296
297 # Test that we have valid options
298 if [ "$cmd" = a -o "$cmd" = d ]; then
299         if [ -z "$iface" ]; then
300                 usage "Interface not specified"
301         fi
302 elif [ "$cmd" != u ]; then
303         [ -n "$cmd" -a "$cmd" != h ] && usage "Unknown option $cmd"
304         usage
305 fi
306 if [ "$cmd" = a ]; then
307         for x in '/' \\ ' ' '*'; do
308                 case "$iface" in
309                 *[$x]*) error_exit "$x not allowed in interface name";;
310                 esac
311         done
312         for x in '.' '-' '~'; do
313                 case "$iface" in
314                 [$x]*) error_exit \
315                         "$x not allowed at start of interface name";;
316                 esac
317         done
318         [ "$cmd" = a -a -t 0 ] && error_exit "No file given via stdin"
319 fi
320
321 if [ ! -d "$IFACEDIR" ]; then
322         if [ ! -d "$VARDIR" ]; then
323                 if [ -L "$VARDIR" ]; then
324                         dir="$(readlink "$VARDIR")"
325                         # link maybe relative
326                         cd "${VARDIR%/*}"
327                         if ! mkdir -m 0755 -p "$dir"; then
328                                 error_exit "Failed to create needed" \
329                                         "directory $dir"
330                         fi
331                 else
332                         if ! mkdir -m 0755 -p "$VARDIR"; then
333                                 error_exit "Failed to create needed" \
334                                         "directory $VARDIR"
335                         fi
336                 fi
337         fi
338         mkdir -m 0755 -p "$IFACEDIR" || \
339                 error_exit "Failed to create needed directory $IFACEDIR"
340 else
341         # Delete any existing information about the interface
342         if [ "$cmd" = d ]; then
343                 cd "$IFACEDIR"
344                 for i in $args; do
345                         if [ "$cmd" = d -a ! -e "$i" ]; then
346                                 $force && continue
347                                 error_exit "No resolv.conf for" \
348                                         "interface $i"
349                         fi
350                         rm -f "$i" "$METRICDIR/"*" $i" \
351                                 "$PRIVATEDIR/$i" || exit $?
352                 done
353         fi
354 fi
355
356 if [ "$cmd" = a ]; then
357         # Read resolv.conf from stdin
358         resolv="$(cat)\n"
359         # If what we are given matches what we have, then do nothing
360         if [ -e "$IFACEDIR/$iface" ]; then
361                 if [ "$(printf "$resolv")" = \
362                         "$(cat "$IFACEDIR/$iface")" ]
363                 then
364                         exit 0
365                 fi
366                 rm "$IFACEDIR/$iface"
367         fi
368         printf "$resolv" >"$IFACEDIR/$iface" || exit $?
369         [ ! -d "$METRICDIR" ] && mkdir "$METRICDIR"
370         rm -f "$METRICDIR/"*" $iface"
371         if [ -n "$IF_METRIC" ]; then
372                 # Pad metric to 6 characters, so 5 is less than 10
373                 while [ ${#IF_METRIC} -le 6 ]; do
374                         IF_METRIC="0$IF_METRIC"
375                 done
376                 echo " " >"$METRICDIR/$IF_METRIC $iface"
377         fi
378         case "$IF_PRIVATE" in
379         [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
380                 if [ ! -d "$PRIVATEDIR" ]; then
381                         [ -e "$PRIVATEDIR" ] && rm "$PRIVATEDIR"
382                         mkdir "$PRIVATEDIR"
383                 fi
384                 [ -d "$PRIVATEDIR" ] && echo " " >"$PRIVATEDIR/$iface"
385                 ;;
386         *)
387                 if [ -e "$PRIVATEDIR/$iface" ]; then
388                         rm -f "$PRIVATEDIR/$iface"
389                 fi
390                 ;;
391         esac
392 fi
393
394 eval "$(make_vars)"
395 export RESOLVCONF DOMAINS SEARCH NAMESERVERS
396 : ${list_resolv:=list_resolv -l}
397 retval=0
398 for script in "$LIBEXECDIR"/*; do
399         if [ -f "$script" ]; then
400                 if [ -x "$script" ]; then
401                         "$script" "$cmd" "$iface"
402                 else
403                         (. "$script" "$cmd" "$fiace")
404                 fi
405                 retval=$(($retval + $?))
406         fi
407 done
408 exit $retval