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