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