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