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