xref: /freebsd/usr.sbin/bsdconfig/networking/share/resolv.subr (revision 8a166cafe0965f6bd72cd3d2f5372704f05cb5e8)
1if [ ! "$_NETWORKING_RESOLV_SUBR" ]; then _NETWORKING_RESOLV_SUBR=1
2#
3# Copyright (c) 2006-2012 Devin Teske
4# All Rights Reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27# $FreeBSD$
28#
29############################################################ INCLUDES
30
31BSDCFG_SHARE="/usr/share/bsdconfig"
32. $BSDCFG_SHARE/common.subr || exit 1
33f_dprintf "%s: loading includes..." networking/resolv.subr
34f_include $BSDCFG_SHARE/dialog.subr
35f_include $BSDCFG_SHARE/strings.subr
36f_include $BSDCFG_SHARE/networking/common.subr
37f_include $BSDCFG_SHARE/networking/ipaddr.subr
38
39BSDCFG_LIBE="/usr/libexec/bsdconfig" APP_DIR="120.networking"
40f_include_lang $BSDCFG_LIBE/$APP_DIR/include/messages.subr
41
42############################################################ CONFIGURATION
43
44#
45# Path to resolv.conf(5).
46#
47: ${RESOLV_CONF:="/etc/resolv.conf"}
48
49#
50# When updating resolv.conf(5), should we populate the `search' directive with
51# all possible sub-domains? In example, if the domain is "sub.domain.com", when
52# the below option is set to 1, include both "sub.domain.com" and "domain.com"
53# in the `search' directive, otherwise use only "sub.domain.com".
54#
55# When enabled (set to 1), specify the minimum number of dots required for each
56# `search' domain by setting the second option below, `RESOLVER_SEARCH_NDOTS'.
57#
58: ${RESOLVER_SEARCH_DOMAINS_ALL:=1}
59: ${RESOLVER_SEARCH_NDOTS:=1}
60
61############################################################ FUNCTIONS
62
63# f_resolv_conf_domain
64#
65# Returns the domain configured in resolv.conf(5).
66#
67f_resolv_conf_domain()
68{
69	tail -r "$RESOLV_CONF" 2> /dev/null | awk \
70	'
71		BEGIN { found = 0 }
72		( tolower($1) == "domain" ) \
73		{
74			print $2
75			found = 1
76			exit
77		}
78		END { exit ! found }
79	'
80}
81
82# f_resolv_conf_search
83#
84# Returns the search configured in resolv.conf(5).
85#
86f_resolv_conf_search()
87{
88	tail -r "$RESOLV_CONF" 2> /dev/null | awk \
89	'
90		BEGIN { found = 0 }
91		{
92			tl0 = tolower($0)
93			if ( match(tl0, /^[[:space:]]*search[[:space:]]+/) ) {
94				search = substr($0, RLENGTH + 1)
95				sub(/[[:space:]]*#.*$/, "", search)
96				gsub(/[[:space:]]+/, " ", search)
97				print search
98				found = 1
99				exit
100			}
101		}
102		END { exit ! found }
103	'
104}
105
106# f_resolv_conf_nameservers
107#
108# Returns nameserver(s) configured in resolv.conf(5).
109#
110f_resolv_conf_nameservers()
111{
112	awk \
113	'
114		BEGIN { found = 0 }
115		( $1 == "nameserver" ) \
116		{
117			print $2
118			found = 1
119		}
120		END { exit ! found }
121	' \
122	"$RESOLV_CONF" 2> /dev/null
123}
124
125# f_dialog_resolv_conf_update $hostname
126#
127# Updates the search/domain directives in resolv.conf(5) given a valid fully-
128# qualified hostname.
129#
130# This function is a two-parter. Below is the awk(1) portion of the function,
131# afterward is the sh(1) function which utilizes the below awk script.
132#
133f_dialog_resolv_conf_update_awk='
134# Variables that should be defined on the invocation line:
135# 	-v domain="domain"
136# 	-v search_all="0|1"
137# 	-v search_ndots="1+"
138#
139BEGIN {
140	domain_found = search_found = 0
141
142	if ( search_all ) {
143		search = ""
144		subdomain = domain
145		if ( search_ndots < 1 )
146			search_ndots = 1
147
148		ndots = split(subdomain, labels, ".") - 1
149		while ( ndots-- >= search_ndots ) {
150			if ( length(search) ) search = search " "
151			search = search subdomain
152			sub(/[^.]*\./, "", subdomain)
153		}
154	}
155	else search = domain
156}
157{
158	if ( domain_found && search_found ) { print; next }
159
160	tl0 = tolower($0)
161	if ( ! domain_found && \
162	     match(tl0, /^[[:space:]]*domain[[:space:]]+/) ) \
163	{
164		if ( length(domain) ) {
165			printf "%s%s\n", substr($0, 0, RLENGTH), domain
166			domain_found = 1
167		}
168	}
169	else if ( ! search_found && \
170	          match(tl0, /^[[:space:]]*search[[:space:]]+/) ) \
171	{
172		if ( length(search) ) {
173			printf "%s%s\n", substr($0, 0, RLENGTH), search
174			search_found = 1
175		}
176	}
177	else print
178}
179END {
180	if ( ! search_found && length(search) )
181		printf "search\t%s\n", search
182	if ( ! domain_found && length(domain) )
183		printf "domain\t%s\n", domain
184}
185'
186f_dialog_resolv_conf_update()
187{
188	local hostname="$1"
189
190	#
191	# Extrapolate the desired domain search parameter for resolv.conf(5)
192	#
193	local search ndots domain="${hostname#*.}"
194	if [ "$RESOLVER_SEARCH_DOMAINS_ALL" = "1" ]; then
195		search=""
196		ndots=$( IFS=.; set -- $domain; echo $(( $# - 1 )) )
197		while [ $ndots -ge ${RESOLVER_SEARCH_NDOTS:-1} ]; do
198			search="$search${search:+ }$domain"
199			domain="${domain#*.}"
200			ndots=$(( $ndots - 1 ))
201		done
202		domain="${hostname#*.}"
203	else
204		search="$domain"
205	fi
206
207	#
208	# Save domain/search information only if different from resolv.conf(5)
209	#
210	if [ "$domain" != "$( f_resolv_conf_domain )" -o \
211	     "$search" != "$( f_resolv_conf_search )" ]
212	then
213		f_dialog_info "Saving new domain/search settings" \
214		              "to resolv.conf(5)..."
215
216		#
217		# Create a new temporary file to write our resolv.conf(5)
218		# update with our new `domain' and `search' directives.
219		#
220		local tmpfile="$( mktemp -t "$pgm" )"
221		[ "$tmpfile" ] || return $FAILURE
222
223		#
224		# Fixup permissions and ownership (mktemp(1) creates the
225		# temporary file with 0600 permissions -- change the
226		# permissions and ownership to match resolv.conf(5) before
227		# we write it out and mv(1) it into place).
228		#
229		local mode="$( stat -f '%#Lp' "$RESOLV_CONF" 2> /dev/null )"
230		local owner="$( stat -f '%u:%g' "$RESOLV_CONF" 2> /dev/null )"
231		f_quietly chmod "${mode:-0644}" "$tmpfile"
232		f_quietly chown "${owner:-root:wheel}" "$tmpfile"
233
234		#
235		# Operate on resolv.conf(5), replacing only the last
236		# occurrences of `domain' and `search' directives (or add
237		# them to the top if not found), in strict-adherence to the
238		# following entry in resolver(5):
239		#
240		# 	The domain and search keywords are mutually exclusive.
241		# 	If more than one instance of these keywords is present,
242		# 	the last instance will override.
243		#
244		# NOTE: If RESOLVER_SEARCH_DOMAINS_ALL is set to `1' in the
245		# environment, all sub-domains will be added to the `search'
246		# directive, not just the FQDN.
247		#
248		local domain="${hostname#*.}" new_contents
249		[ "$domain" = "$hostname" ] && domain=
250		new_contents=$( tail -r "$RESOLV_CONF" 2> /dev/null )
251		new_contents=$( echo "$new_contents" | awk \
252			-v domain="$domain" \
253			-v search_all="${RESOLVER_SEARCH_DOMAINS_ALL:-1}" \
254			-v search_ndots="${RESOLVER_SEARCH_NDOTS:-1}" \
255			"$f_dialog_resolv_conf_update_awk" )
256
257		#
258		# Write the temporary file contents and move the temporary
259		# file into place.
260		#
261		echo "$new_contents" | tail -r > "$tmpfile" || return $FAILURE
262		f_quietly mv "$tmpfile" "$RESOLV_CONF"
263
264	fi
265}
266
267# f_dialog_input_nameserver [ $n $nameserver ]
268#
269# Allows the user to edit a given nameserver. The first argument is the
270# resolv.conf(5) nameserver ``instance'' integer. For example, this will be one
271# if editing the first nameserver instance, two if editing the second, three if
272# the third, ad nauseum. If this argument is zero, null, or missing, the value
273# entered by the user (if non-null) will be added to resolv.conf(5) as a new
274# `nameserver' entry. The second argument is the IPv4 address of the nameserver
275# to be edited -- this will be displayed as the initial value during the edit.
276#
277# Taint-checking is performed when editing an existing entry (when the second
278# argument is one or higher) in that the first argument must match the current
279# value of the Nth `nameserver' instance in resolv.conf(5) else an error is
280# generated discarding any/all changes.
281#
282# This function is a two-parter. Below is the awk(1) portion of the function,
283# afterward is the sh(1) function which utilizes the below awk script.
284#
285f_dialog_input_nameserver_edit_awk='
286# Variables that should be defined on the invocation line:
287# 	-v nsindex="1+"
288# 	-v old_value="..."
289# 	-v new_value="..."
290#
291BEGIN {
292	if ( nsindex < 1 ) exit 1
293	found = n = 0
294}
295{
296	if ( found ) { print; next }
297
298	if ( match(tolower($0), /^[[:space:]]*nameserver[[:space:]]+/)) {
299		if ( ++n == nsindex ) {
300			if ( $2 != old_value ) exit 2
301			if ( new_value != "" ) printf "%s%s\n", \
302				substr($0, 0, RLENGTH), new_value
303			found = 1
304		}
305		else print
306	}
307	else print
308}
309END { if ( ! found ) exit 3 }
310'
311f_dialog_input_nameserver()
312{
313	local index="${1:-0}" old_ns="$2" new_ns
314	local ns="$old_ns"
315
316	#
317	# Perform sanity checks
318	#
319	f_isinteger "$index" || return $FAILURE
320	[ $index -ge 0 ] || return $FAILURE
321
322	local msg
323	if [ $index -gt 0 ]; then
324		if [ "$USE_XDIALOG" ]; then
325			msg="$xmsg_please_enter_nameserver_existing"
326		else
327			msg="$msg_please_enter_nameserver_existing"
328		fi
329	else
330		msg="$msg_please_enter_nameserver"
331	fi
332
333	#
334	# Loop until the user provides taint-free input.
335	#
336	while :; do
337		new_ns=$( f_dialog_input "$msg" "$ns" \
338		                         "$hline_num_punc_tab_enter"
339		        ) || return
340
341		# Take only the first "word" of the user's input
342		new_ns="${new_ns%%[$IFS]*}"
343
344		# Taint-check the user's input
345		[ "$new_ns" ] || break
346		f_dialog_validate_ipaddr "$new_ns" && break
347
348		# Update prompt to allow user to re-edit previous entry
349		ns="$new_ns"
350	done
351
352	#
353	# Save only if the user changed the nameserver.
354	#
355	if [ $index -eq "0" -a "$new_ns" ]; then
356		f_dialog_info "$msg_saving_nameserver"
357		printf "nameserver\t%s\n" "$new_ns" >> "$RESOLV_CONF"
358		return $SUCCESS
359	elif [ $index -gt 0 -a "$old_ns" != "$new_ns" ]; then
360		if [ "$new_ns" ]; then
361			msg="$msg_saving_nameserver_existing"
362		else
363			msg="$msg_removing_nameserver"
364		fi
365		f_dialog_info "$msg"
366
367		#
368		# Create a new temporary file to write our new resolv.conf(5)
369		#
370		local tmpfile="$( mktemp -t "$pgm" )"
371		[ "$tmpfile" ] || return $FAILURE
372
373		#
374		# Quietly fixup permissions and ownership
375		#
376		local mode owner
377		mode=$( stat -f '%#Lp' "$RESOLV_CONF" 2> /dev/null )
378		owner=$( stat -f '%u:%g' "$RESOLV_CONF" 2> /dev/null )
379		f_quietly chmod "${mode:-0644}" "$tmpfile"
380		f_quietly chown "${owner:-root:wheel}" "$tmpfile"
381
382		#
383		# Operate on resolv.conf(5)
384		#
385		local new_contents
386		new_contents=$( awk -v nsindex="$index"    \
387		                    -v old_value="$old_ns" \
388		                    -v new_value="$new_ns" \
389		                    "$f_dialog_input_nameserver_edit_awk" \
390		                    "$RESOLV_CONF" )
391
392		#
393		# Produce an appropriate error message if necessary.
394		#
395		local retval=$?
396		case $retval in
397		1) f_die 1 "$msg_internal_error_nsindex_value" "$nsindex";;
398		2) f_dialog_msgbox "$msg_resolv_conf_changed_while_editing"
399		   return $retval;;
400		3) f_dialog_msgbox "$msg_resolv_conf_entry_no_longer_exists"
401		   return $retval;;
402		esac
403
404		#
405		# Write the temporary file contents and move the temporary
406		# file into place.
407		#
408		echo "$new_contents" > "$tmpfile" || return $FAILURE
409		f_quietly mv "$tmpfile" "$RESOLV_CONF"
410	fi
411}
412
413# f_dialog_menu_nameservers
414#
415# Edit the nameservers in resolv.conf(5).
416#
417f_dialog_menu_nameservers()
418{
419	local opt_exit="$msg_return_to_previous_menu"
420	local opt_add="$msg_add_nameserver"
421	local hline="$hline_arrows_tab_enter"
422	local prompt size
423
424	#
425	# Loop forever until the user has finished configuring nameservers
426	#
427	prompt="$msg_dns_configuration"
428	while :; do
429		#
430		# Re/Build list of nameservers
431		#
432		local nameservers="$( f_resolv_conf_nameservers )"
433		local menu_list="$(
434			index=1
435
436			echo "'X $msg_exit' '$opt_exit'"
437			index=$(( $index + 1 ))
438
439			echo "'A $msg_add'  '$opt_add'"
440			index=$(( $index + 1 ))
441
442			for ns in $nameservers; do
443				[ $index -lt ${#DIALOG_MENU_TAGS} ] || break
444				tag=$( f_substr "$DIALOG_MENU_TAGS" $index 1 )
445				echo "'$tag nameserver' '$ns'"
446				index=$(( $index + 1 ))
447			done
448		)"
449
450		#
451		# Display configuration-edit menu
452		#
453		size=$( eval f_dialog_menu_size \
454		        	\"\$DIALOG_TITLE\"     \
455		        	\"\$DIALOG_BACKTITLE\" \
456		        	\"\$prompt\"           \
457		        	\"\$hline\"            \
458		        	$menu_list             )
459		local dialog_menu
460		dialog_menu=$( eval $DIALOG \
461			--clear --title \"\$DIALOG_TITLE\" \
462			--backtitle \"\$DIALOG_BACKTITLE\" \
463			--hline \"\$hline\"                \
464			--ok-label \"\$msg_ok\"            \
465			--cancel-label \"\$msg_cancel\"    \
466			--menu \"\$prompt\" $size          \
467			$menu_list                         \
468			2>&1 >&$DIALOG_TERMINAL_PASSTHRU_FD
469		)
470
471		local retval=$?
472		setvar DIALOG_MENU_$$ "$dialog_menu"
473		local tag="$( f_dialog_menutag )" ns=""
474
475		# Return if "Cancel" was chosen (-1) or ESC was pressed (255)
476		[ $retval -eq $SUCCESS ] || return $retval
477
478		case "$tag" in
479		"X $msg_exit") break;;
480		"A $msg_add")
481			f_dialog_input_nameserver
482			;;
483		*)
484			n=$( eval f_dialog_menutag2index \"\$tag\" $menu_list )
485			ns=$( eval f_dialog_menutag2item \"\$tag\" $menu_list )
486			f_dialog_input_nameserver $(( $n - 2 )) "$ns"
487			;;
488		esac
489	done
490}
491
492############################################################ MAIN
493
494f_dprintf "%s: Successfully loaded." networking/resolv.subr
495
496fi # ! $_NETWORKING_RESOLV_SUBR
497