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