xref: /freebsd/share/examples/jails/jng (revision bbb51924bb60b8c8f7dfbb7069b1511c312b9b57)
1#!/bin/sh
2#-
3# Copyright (c) 2016 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############################################################ IDENT(1)
30#
31# $Title: netgraph(4) management script for vnet jails $
32#
33############################################################ INFORMATION
34#
35# Use this tool with jail.conf(5) (or rc.conf(5) ``legacy'' configuration) to
36# manage `vnet' interfaces. In jail.conf(5) format:
37#
38# ### BEGIN EXCERPT ###
39#
40# xxx {
41# 	host.hostname = "xxx.yyy";
42# 	path = "/vm/xxx";
43#
44# 	#
45# 	# NB: Below 2-lines required
46# 	# NB: The number of ngN_xxx interfaces should match the number of
47# 	#     arguments given to `jng bridge xxx' in exec.prestart value.
48# 	#
49# 	vnet;
50# 	vnet.interface = "ng0_xxx ng1_xxx ...";
51#
52# 	exec.clean;
53# 	exec.system_user = "root";
54# 	exec.jail_user = "root";
55#
56# 	#
57# 	# NB: Below 2-lines required
58# 	# NB: The number of arguments after `jng bridge xxx' should match
59# 	#     the number of ngN_xxx arguments in vnet.interface value.
60# 	#
61# 	exec.prestart += "jng bridge xxx em0 em1 ...";
62# 	exec.poststop += "jng shutdown xxx";
63#
64# 	# Standard recipe
65# 	exec.start += "/bin/sh /etc/rc";
66# 	exec.stop = "/bin/sh /etc/rc.shutdown";
67# 	exec.consolelog = "/var/log/jail_xxx_console.log";
68# 	mount.devfs;
69#
70# 	# Optional (default off)
71# 	#allow.mount;
72# 	#allow.set_hostname = 1;
73# 	#allow.sysvipc = 1;
74# 	#devfs_ruleset = "11"; # rule to unhide bpf for DHCP
75# }
76#
77# ### END EXCERPT ###
78#
79# In rc.conf(5) ``legacy'' format (used when /etc/jail.conf does not exist):
80#
81# ### BEGIN EXCERPT ###
82#
83# jail_enable="YES"
84# jail_list="xxx"
85#
86# #
87# # Global presets for all jails
88# #
89# jail_devfs_enable="YES"	# mount devfs
90#
91# #
92# # Global options (default off)
93# #
94# #jail_mount_enable="YES"		# mount /etc/fstab.{name}
95# #jail_set_hostname_allow="YES"	# Allow hostname to change
96# #jail_sysvipc_allow="YES"		# Allow SysV Interprocess Comm.
97#
98# # xxx
99# jail_xxx_hostname="xxx.shxd.cx"		# hostname
100# jail_xxx_rootdir="/vm/xxx"			# root directory
101# jail_xxx_vnet_interfaces="ng0_xxx ng1xxx ..."	# vnet interface(s)
102# jail_xxx_exec_prestart0="jng bridge xxx em0 em1 ..."	# bridge interface(s)
103# jail_xxx_exec_poststop0="jng shutdown xxx"	# destroy interface(s)
104# #jail_xxx_mount_enable="YES"			# mount /etc/fstab.xxx
105# #jail_xxx_devfs_ruleset="11"			# rule to unhide bpf for DHCP
106#
107# ### END EXCERPT ###
108#
109# Note that the legacy rc.conf(5) format is converted to
110# /var/run/jail.{name}.conf by /etc/rc.d/jail if jail.conf(5) is missing.
111#
112# ASIDE: dhclient(8) inside a vnet jail...
113#
114# To allow dhclient(8) to work inside a vnet jail, make sure the following
115# appears in /etc/devfs.rules (which should be created if it doesn't exist):
116#
117# 	[devfsrules_jail=11]
118# 	add include $devfsrules_hide_all
119# 	add include $devfsrules_unhide_basic
120# 	add include $devfsrules_unhide_login
121# 	add include $devfsrules_unhide_bpf
122#
123# And set ether devfs.ruleset="11" (jail.conf(5)) or
124# jail_{name}_devfs_ruleset="11" (rc.conf(5)).
125#
126# NB: While this tool can't create every type of desirable topology, it should
127# handle most setups, minus some which considered exotic or purpose-built.
128#
129############################################################ GLOBALS
130
131pgm="${0##*/}" # Program basename
132
133#
134# Global exit status
135#
136SUCCESS=0
137FAILURE=1
138
139############################################################ FUNCTIONS
140
141usage()
142{
143	local action usage descr
144	exec >&2
145	echo "Usage: $pgm action [arguments]"
146	echo "Actions:"
147	for action in \
148		bridge		\
149		graph		\
150		show		\
151		show1		\
152		shutdown	\
153	; do
154		eval usage=\"\$jng_${action}_usage\"
155		[ "$usage" ] || continue
156		eval descr=\"\$jng_${action}_descr\"
157		printf "\t%s\n\t\t%s\n" "$usage" "$descr"
158	done
159	exit $FAILURE
160}
161
162action_usage()
163{
164	local usage action="$1"
165	eval usage=\"\$jng_${action}_usage\"
166	echo "Usage: $pgm $usage" >&2
167	exit $FAILURE
168}
169
170mustberoot_to_continue()
171{
172	if [ "$( id -u )" -ne 0 ]; then
173		echo "Must run as root!" >&2
174		exit $FAILURE
175	fi
176}
177
178jng_bridge_usage="bridge [-b BRIDGE_NAME] NAME interface0 [interface1 ...]"
179jng_bridge_descr="Create ng0_NAME [ng1_NAME ...]"
180jng_bridge()
181{
182	local OPTIND=1 OPTARG flag bridge=bridge
183	while getopts b: flag; do
184		case "$flag" in
185		b) bridge="$OPTARG"
186		   [ "$bridge" ] || action_usage bridge ;; # NOTREACHED
187		*) action_usage bridge # NOTREACHED
188		esac
189	done
190	shift $(( $OPTIND - 1 ))
191
192	local name="$1"
193	[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -gt 1 ] ||
194		action_usage bridge # NOTREACHED
195	shift 1 # name
196
197	mustberoot_to_continue
198
199	local iface iface_devid eiface eiface_devid
200	local new num quad i=0
201	for iface in $*; do
202
203		# 0. Make sure the interface doesn't exist already
204		eiface=ng${i}_$name
205		ngctl msg "$eiface:" getifname > /dev/null 2>&1 && continue
206
207		# 1. Bring the interface up
208		ifconfig $iface up || return
209
210		# 2. Set promiscuous mode and don't overwrite src addr
211		ngctl msg $iface: setpromisc 1 || return
212		ngctl msg $iface: setautosrc 0 || return
213
214		# 3. Make sure the interface has been bridged
215		if ! ngctl info ${iface}bridge: > /dev/null 2>&1; then
216			ngctl mkpeer $iface: bridge lower link0 || return
217			ngctl connect $iface: $iface:lower upper link1 ||
218				return
219			ngctl name $iface:lower ${iface}bridge || return
220		fi
221
222		# 3.5. Optionally create a secondary bridge
223		if [ "$bridge" != "bridge" ] &&
224		   ! ngctl info "$iface$bridge:" > /dev/null 2>&1
225		then
226			num=2
227			while ngctl msg ${iface}bridge: getstats $num \
228				> /dev/null 2>&1
229			do
230				num=$(( $num + 1 ))
231			done
232			ngctl mkpeer $iface:lower bridge link$num link1 ||
233				return
234			ngctl name ${iface}bridge:link$num "$iface$bridge" ||
235				return
236		fi
237
238		# 4. Create a new interface to the bridge
239		num=2
240		while ngctl msg "$iface$bridge:" getstats $num > /dev/null 2>&1
241		do
242			num=$(( $num + 1 ))
243		done
244		ngctl mkpeer "$iface$bridge:" eiface link$num ether || return
245
246		# 5. Rename the new interface
247		while [ ${#eiface} -gt 15 ]; do # OS limitation
248			eiface=${eiface%?}
249		done
250		new=$( set -- `ngctl show -n "$iface$bridge:link$num"` &&
251			echo $2 ) || return
252		ngctl name "$iface$bridge:link$num" $eiface || return
253		ifconfig $new name $eiface || return
254
255		#
256		# 6. Set the MAC address of the new interface using a sensible
257		# algorithm to prevent conflicts on the network.
258		#
259		# The formula I'm using is ``SP:SS:SI:II:II:II'' where:
260		# + S denotes 16 bits of sum(1) data, split because P (below).
261		# + P denotes the special nibble whose value, if one of
262		#   2, 6, A, or E (but usually 2) denotes a privately
263		#   administered MAC address (while remaining routable).
264		# + I denotes bits that are inherited from parent interface.
265		#
266		# The S bits are a CRC-16 checksum of NAME, allowing the jail
267		# to change link numbers in ng_bridge(4) without affecting the
268		# MAC address. Meanwhile, if the jail NAME changes (e.g., it
269		# was duplicated and given a new name with no other changes),
270		# the underlying network interface changes, or the jail is
271		# moved to another host, the MAC address will be recalculated
272		# to a new, similarly unique value preventing conflict.
273		#
274		iface_devid=$( ifconfig $iface ether | awk '/ether/,$0=$2' )
275		eiface_devid=${iface_devid#??:??:?}
276		num=$( set -- `echo -n $name | sum` && echo $1 )
277		quad=$(( $num & 15 ))
278		case "$quad" in
279		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
280		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
281		esac
282		eiface_devid=:$quad$eiface_devid
283		num=$(( $num >> 4 ))
284		quad=$(( $num & 15 ))
285		case "$quad" in
286		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
287		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
288		esac
289		eiface_devid=$quad$eiface_devid
290		num=$(( $num >> 4 ))
291		quad=$(( $num & 15 ))
292		case "$quad" in
293		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
294		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
295		esac
296		eiface_devid=2:$quad$eiface_devid
297		num=$(( $num >> 4 ))
298		quad=$(( $num & 15 ))
299		case "$quad" in
300		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
301		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
302		esac
303		eiface_devid=$quad$eiface_devid
304		ifconfig $eiface ether $eiface_devid > /dev/null 2>&1
305
306		i=$(( $i + 1 )) # on to next ng{i}_name
307	done # for iface
308}
309
310jng_graph_usage="graph [-f] [-T type] [-o output]"
311jng_graph_descr="Generate network graph (default output is \`jng.svg')"
312jng_graph()
313{
314	local OPTIND=1 OPTARG flag
315	local output=jng.svg output_type= force=
316	while getopts fo:T: flag; do
317		case "$flag" in
318		f) force=1 ;;
319		o) output="$OPTARG" ;;
320		T) output_type="$OPTARG" ;;
321		*) action_usage graph # NOTREACHED
322		esac
323	done
324	shift $(( $OPTIND - 1 ))
325	[ $# -eq 0 -a "$output" ] || action_usage graph # NOTREACHED
326	mustberoot_to_continue
327	if [ -e "$output" -a ! "$force" ]; then
328		echo "$output: Already exists (use \`-f' to overwrite)" >&2
329		return $FAILURE
330	fi
331	if [ ! "$output_type" ]; then
332		local valid suffix
333		valid=$( dot -Txxx 2>&1 )
334		for suffix in ${valid##*:}; do
335			[ "$output" != "${output%.$suffix}" ] || continue
336			output_type=$suffix
337			break
338		done
339	fi
340	ngctl dot | dot ${output_type:+-T "$output_type"} -o "$output"
341}
342
343jng_show_usage="show"
344jng_show_descr="List possible NAME values for \`show NAME'"
345jng_show1_usage="show NAME"
346jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]"
347jng_show2_usage="show [NAME]"
348jng_show()
349{
350	local OPTIND=1 OPTARG flag
351	while getopts "" flag; do
352		case "$flag" in
353		*) action_usage show2 # NOTREACHED
354		esac
355	done
356	shift $(( $OPTIND - 1 ))
357	mustberoot_to_continue
358	if [ $# -eq 0 ]; then
359		ngctl ls | awk '$4=="bridge",$0=$2' |
360			xargs -rn1 -Ibridge ngctl show bridge: |
361			awk 'sub(/^ng[[:digit:]]+_/, "", $2), $0 = $2' |
362			sort -u
363		return
364	fi
365	ngctl ls | awk -v name="$1" '
366		match($2, /^ng[[:digit:]]+_/) &&
367			substr($2, RSTART + RLENGTH) == name &&
368			$4 == "eiface", $0 = $2
369	' | sort
370}
371
372jng_shutdown_usage="shutdown NAME"
373jng_shutdown_descr="Shutdown ng0_NAME [ng1_NAME ...]"
374jng_shutdown()
375{
376	local OPTIND=1 OPTARG flag
377	while getopts "" flag; do
378		case "$flag" in
379		*) action_usage shutdown # NOTREACHED
380		esac
381	done
382	shift $(( $OPTIND -1 ))
383	local name="$1"
384	[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -eq 1 ] ||
385		action_usage shutdown # NOTREACHED
386	mustberoot_to_continue
387	jng_show "$name" | xargs -rn1 -I eiface ngctl shutdown eiface:
388}
389
390############################################################ MAIN
391
392#
393# Command-line arguments
394#
395action="$1"
396[ "$action" ] || usage # NOTREACHED
397
398#
399# Validate action argument
400#
401if [ "$BASH_VERSION" ]; then
402	type="$( type -t "jng_$action" )" || usage # NOTREACHED
403else
404	type="$( type "jng_$action" 2> /dev/null )" || usage # NOTREACHED
405fi
406case "$type" in
407*function)
408	shift 1 # action
409	eval "jng_$action" \"\$@\"
410	;;
411*) usage # NOTREACHED
412esac
413
414################################################################################
415# END
416################################################################################
417