xref: /freebsd/share/examples/jails/jib (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: if_bridge(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 eNb_xxx interfaces should match the number of
47# 	#     arguments given to `jib addm xxx' in exec.prestart value.
48# 	#
49# 	vnet;
50# 	vnet.interface = "e0b_xxx e1b_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 `jib addm xxx' should match
59# 	#     the number of eNb_xxx arguments in vnet.interface value.
60# 	#
61# 	exec.prestart += "jib addm xxx em0 em1 ...";
62# 	exec.poststop += "jib destroy 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="e0b_xxx e1bxxx ..."	# vnet interface(s)
102# jail_xxx_exec_prestart0="jib addm xxx em0 em1 ..."	# bridge interface(s)
103# jail_xxx_exec_poststop0="jib destroy 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		addm		\
149		show		\
150		show1		\
151		destroy		\
152	; do
153		eval usage=\"\$jib_${action}_usage\"
154		[ "$usage" ] || continue
155		eval descr=\"\$jib_${action}_descr\"
156		printf "\t%s\n\t\t%s\n" "$usage" "$descr"
157	done
158	exit $FAILURE
159}
160
161action_usage()
162{
163	local usage action="$1"
164	eval usage=\"\$jib_${action}_usage\"
165	echo "Usage: $pgm $usage" >&2
166	exit $FAILURE
167}
168
169mustberoot_to_continue()
170{
171	if [ "$( id -u )" -ne 0 ]; then
172		echo "Must run as root!" >&2
173		exit $FAILURE
174	fi
175}
176
177jib_addm_usage="addm [-b BRIDGE_NAME] NAME interface0 [interface1 ...]"
178jib_addm_descr="Creates e0b_NAME [e1b_NAME ...]"
179jib_addm()
180{
181	local OPTIND=1 OPTARG flag bridge=bridge
182	while getopts b: flag; do
183		case "$flag" in
184		b) bridge="${OPTARG:-bridge}" ;;
185		*) action_usage addm # NOTREACHED
186		esac
187	done
188	shift $(( $OPTIND - 1 ))
189
190	local name="$1"
191	[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -gt 1 ] ||
192		action_usage addm # NOTREACHED
193	shift 1 # name
194
195	mustberoot_to_continue
196
197	local iface iface_devid eiface_devid_a eiface_devid_b
198	local new num quad i=0
199	for iface in $*; do
200
201		# 1. Make sure the interface doesn't exist already
202		ifconfig "e${i}a_$name" > /dev/null 2>&1 && continue
203
204		# 2. Bring the interface up
205		ifconfig $iface up || return
206
207		# 3. Make sure the interface has been bridged
208		if ! ifconfig "$iface$bridge" > /dev/null 2>&1; then
209			new=$( ifconfig bridge create ) || return
210			ifconfig $new addm $iface || return
211			ifconfig $new name "$iface$bridge" || return
212		fi
213
214		# 4. Create a new interface to the bridge
215		new=$( ifconfig epair create ) || return
216		ifconfig "$iface$bridge" addm $new || return
217
218		# 5. Rename the new interface
219		ifconfig $new name "e${i}a_$name" || return
220		ifconfig ${new%a}b name "e${i}b_$name" || return
221
222		#
223		# 6. Set the MAC address of the new interface using a sensible
224		# algorithm to prevent conflicts on the network.
225		#
226		# The formula I'm using is ``SP:SS:SI:II:II:II'' where:
227		# + S denotes 16 bits of sum(1) data, split because P (below).
228		# + P denotes the special nibble whose value, if one of
229		#   2, 6, A, or E (but usually 2) denotes a privately
230		#   administered MAC address (while remaining routable).
231		# + I denotes bits that are inherited from parent interface.
232		#
233		# The S bits are a CRC-16 checksum of NAME, allowing the jail
234		# to change the epair(4) generation order without affecting the
235		# MAC address. Meanwhile, if the jail NAME changes (e.g., it
236		# was duplicated and given a new name with no other changes),
237		# the underlying network interface changes, or the jail is
238		# moved to another host, the MAC address will be recalculated
239		# to a new, similarly unique value preventing conflict.
240		#
241		iface_devid=$( ifconfig $iface ether | awk '/ether/,$0=$2' )
242		eiface_devid_a=${iface_devid#??:??:?}
243		eiface_devid_b=${iface_devid#??:??:?}
244		num=$( set -- `echo -n $name | sum` && echo $1 )
245		quad=$(( $num & 15 ))
246		case "$quad" in
247		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
248		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
249		esac
250		eiface_devid_a=:$quad$eiface_devid_a
251		eiface_devid_b=:$quad$eiface_devid_b
252		num=$(( $num >> 4 ))
253		quad=$(( $num & 15 ))
254		case "$quad" in
255		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
256		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
257		esac
258		eiface_devid_a=$quad$eiface_devid_a
259		eiface_devid_b=$quad$eiface_devid_b
260		num=$(( $num >> 4 ))
261		quad=$(( $num & 15 ))
262		case "$quad" in
263		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
264		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
265		esac
266		eiface_devid_a=2:$quad$eiface_devid_a
267		eiface_devid_b=6:$quad$eiface_devid_b
268		num=$(( $num >> 4 ))
269		quad=$(( $num & 15 ))
270		case "$quad" in
271		10) quad=a ;; 11) quad=b ;; 12) quad=c ;;
272		13) quad=d ;; 14) quad=e ;; 15) quad=f ;;
273		esac
274		eiface_devid_a=$quad$eiface_devid_a
275		eiface_devid_b=$quad$eiface_devid_b
276		ifconfig "e${i}a_$name" ether $eiface_devid_a > /dev/null 2>&1
277		ifconfig "e${i}b_$name" ether $eiface_devid_b > /dev/null 2>&1
278
279		i=$(( $i + 1 )) # on to next ng{i}_name
280	done # for iface
281}
282
283jib_show_usage="show"
284jib_show_descr="List possible NAME values for \`show NAME'"
285jib_show1_usage="show NAME"
286jib_show1_descr="Lists ng0_NAME [ng1_NAME ...]"
287jib_show2_usage="show [NAME]"
288jib_show()
289{
290	local OPTIND=1 OPTARG flag
291	while getopts "" flag; do
292		case "$flag" in
293		*) action_usage show2 # NOTREACHED
294		esac
295	done
296	shift $(( $OPTIND - 1 ))
297	if [ $# -eq 0 ]; then
298		ifconfig | awk '
299			/^[^:[:space:]]+:/ {
300				iface = $1
301				sub(/:.*/, "", iface)
302				next
303			}
304			$1 == "groups:" {
305				for (n = split($0, group); n > 1; n--) {
306					if (group[n] != "bridge") continue
307					print iface
308					next
309				}
310			}' |
311			xargs -rn1 ifconfig |
312			awk '$1 == "member:" &&
313				sub(/^e[[:digit:]]+a_/, "", $2), $0 = $2' |
314			sort -u
315		return
316	fi
317	ifconfig | awk -v name="$1" '
318		match($0, /^e[[:digit:]]+a_/) && sub(/:.*/, "") &&
319			substr($1, RSTART + RLENGTH) == name
320	' | sort
321}
322
323jib_destroy_usage="destroy NAME"
324jib_destroy_descr="Destroy e0b_NAME [e1b_NAME ...]"
325jib_destroy()
326{
327	local OPTIND=1 OPTARG flag
328	while getopts "" flag; do
329		case "$flag" in
330		*) action_usage destroy # NOTREACHED
331		esac
332	done
333	shift $(( $OPTIND -1 ))
334	local name="$1"
335	[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -eq 1 ] ||
336		action_usage destroy # NOTREACHED
337	mustberoot_to_continue
338	jib_show "$name" | xargs -rn1 -I eiface ifconfig eiface destroy
339}
340
341############################################################ MAIN
342
343#
344# Command-line arguments
345#
346action="$1"
347[ "$action" ] || usage # NOTREACHED
348
349#
350# Validate action argument
351#
352if [ "$BASH_VERSION" ]; then
353	type="$( type -t "jib_$action" )" || usage # NOTREACHED
354else
355	type="$( type "jib_$action" 2> /dev/null )" || usage # NOTREACHED
356fi
357case "$type" in
358*function)
359	shift 1 # action
360	eval "jib_$action" \"\$@\"
361	;;
362*) usage # NOTREACHED
363esac
364
365################################################################################
366# END
367################################################################################
368