xref: /freebsd/share/examples/jails/jng (revision 9aff62dee28239f84b4aa4a3429cf26dbbf770cc)
1#!/bin/sh
2#
3# SPDX-License-Identifier: BSD-2-Clause
4#
5# Copyright (c) 2016-2026 Devin Teske <dteske@FreeBSD.org>
6#
7############################################################ IDENT(1)
8#
9# $Title: netgraph(4) management script for vnet jails $
10# $Version: 9.4 $
11#
12############################################################ INFORMATION
13#
14# Use this tool with jail.conf(5) (or rc.conf(5) ``legacy'' configuration) to
15# manage `vnet' interfaces for jails. Designed to automate the creation of vnet
16# interface(s) during jail `prestart', return them to the host during jail
17# `prestop', and destroy said interface(s) during jail `poststop'.
18#
19# In jail.conf(5) format:
20#
21# ### BEGIN EXCERPT ###
22#
23# xxx {
24# 	host.hostname = "xxx.yyy";
25# 	path = "/vm/$name";
26#
27# 	#
28# 	# NB: Below 2-lines required
29# 	# NB: The number of ngN_$name interfaces should match the number of
30# 	#     arguments given to `jng bridge $name' in exec.prestart value.
31# 	#
32# 	vnet;
33# 	vnet.interface = ng0_$name, ng1_$name, ...;
34#
35# 	exec.clean;
36# 	exec.system_user = "root";
37# 	exec.jail_user = "root";
38#
39# 	#
40# 	# NB: Below lines required
41# 	# NB: The number of arguments after `jng bridge $name' should match
42# 	#     the number of ngN_$name arguments in vnet.interface value.
43# 	# NB: Return each ngN_$name to the host in prestop so the kernel does
44# 	#     not move it during jail removal (BPF race). Destroy in poststop.
45# 	#
46# 	exec.prestart += "jng bridge $name em0 em1 ...";
47# 	exec.prestop += "ifconfig ng0_$name -vnet $name";
48# 	exec.prestop += "ifconfig ng1_$name -vnet $name";
49# 	exec.poststop += "jng shutdown $name";
50#
51# 	# Standard recipe
52# 	exec.start += "/bin/sh /etc/rc";
53# 	exec.stop = "/bin/sh /etc/rc.shutdown jail";
54# 	exec.consolelog = "/var/log/jail_${name}_console.log";
55# 	mount.devfs;
56#
57# 	# Optional (default off)
58# 	#allow.mount;
59# 	#allow.set_hostname = 1;
60# 	#allow.sysvipc = 1;
61# 	#devfs_ruleset = "11"; # rule to unhide bpf for DHCP
62# }
63#
64# ### END EXCERPT ###
65#
66# In rc.conf(5) ``legacy'' format (used when /etc/jail.conf does not exist):
67#
68# ### BEGIN EXCERPT ###
69#
70# jail_enable="YES"
71# #jail_confwarn="NO" # Optional: disable warning to migrate to jail.conf(5)
72# jail_list="xxx"
73#
74# #
75# # Global presets for all jails
76# #
77# jail_devfs_enable="YES"	# mount devfs
78#
79# #
80# # Global options (default off)
81# #
82# #jail_mount_enable="YES"		# mount /etc/fstab.{name}
83# #jail_set_hostname_allow="YES"	# Allow hostname to change
84# #jail_sysvipc_allow="YES"		# Allow SysV Interprocess Comm.
85#
86# # xxx
87# jail_xxx_hostname="xxx.shxd.cx"		# hostname
88# jail_xxx_rootdir="/vm/xxx"			# root directory
89# jail_xxx_vnet_interfaces="ng0_xxx ng1xxx ..."	# vnet interface(s)
90# jail_xxx_exec_prestart0="jng bridge xxx em0 em1 ..."	# bridge interface(s)
91# jail_xxx_exec_prestop0="ifconfig ng0_xxx -vnet xxx"	# return ifnet(s)
92# jail_xxx_exec_prestop1="ifconfig ng1_xxx -vnet xxx"
93# jail_xxx_exec_poststop0="jng shutdown xxx"		# destroy interface(s)
94# #jail_xxx_mount_enable="YES"			# mount /etc/fstab.xxx
95# #jail_xxx_devfs_ruleset="11"			# rule to unhide bpf for DHCP
96#
97# ### END EXCERPT ###
98#
99# Note that the legacy rc.conf(5) format is converted to
100# /var/run/jail.{name}.conf by /etc/rc.d/jail if jail.conf(5) is missing.
101#
102# ASIDE: dhclient(8) inside a vnet jail...
103#
104# To allow dhclient(8) to work inside a vnet jail, make sure the following
105# appears in /etc/devfs.rules (which should be created if it doesn't exist):
106#
107# 	[devfsrules_jail=11]
108# 	add include $devfsrules_hide_all
109# 	add include $devfsrules_unhide_basic
110# 	add include $devfsrules_unhide_login
111# 	add path 'bpf*' unhide
112#
113# And set ether devfs.ruleset="11" (jail.conf(5)) or
114# jail_{name}_devfs_ruleset="11" (rc.conf(5)).
115#
116# NB: While this tool can't create every type of desirable topology, it should
117# handle most setups, minus some considered exotic or purpose-built.
118#
119# Uplink on ng_ether(4) `lower' (jng 7+) keeps the WAN MAC table small:
120# ng_bridge(4) does not learn on uplink hooks. The first connected hook being
121# uplink also selects restrictive unknown-unicast: frames for an unknown dest
122# go only to uplink, not to jail links. Inbound unicast to a jail therefore
123# requires that jail's MAC to live in the forwarding database (FDB) on the
124# jail's link. jng 8 pins each eiface MAC with ngctl movehost and sets
125# maxStaleness so host->staleness cannot catch it. `jng pin NAME' replants
126# after an accidental move. ng_bridge must not MOVE_HOST from learnMac=0 hooks
127# or promiscuous TX echo can steal a pinned MAC onto uplink; without that
128# kernel fix, re-run `jng pin'.
129#
130############################################################ CONFIGURATION
131
132#
133# host->staleness is uint16_t; conf.maxStaleness is uint32_t.
134# ng_bridge_timeout expires when ++staleness >= maxStaleness.
135# A threshold above 65535 is unreachable (the counter wraps).
136#
137NG_BRIDGE_MAX_STALENESS=4294967295
138
139############################################################ GLOBALS
140
141VERSION='$Version: 9.4 $'
142
143pgm="${0##*/}" # Program basename
144
145#
146# Global exit status
147#
148SUCCESS=0
149FAILURE=1
150
151#
152# Command-line options
153#
154STATS_FMT=text		# -j for JSON
155
156############################################################ FUNCTIONS
157
158quietly(){ "$@" > /dev/null 2>&1; }
159
160die()
161{
162	local fmt="$1"
163	if [ "$fmt" ]; then
164		shift 1 # fmt
165		printf "%s: $fmt\n" "$pgm" "$@" >&2
166	fi
167	exit $FAILURE
168}
169
170usage()
171{
172	local fmt="$1"
173	local optfmt="\t%-5s %s\n"
174	local action usage descr
175	exec >&2
176	if [ "$fmt" ]; then
177		shift 1 # fmt
178		printf "%s: $fmt\n" "$pgm" "$@"
179	fi
180	printf "Usage: %s [-hv] action [arguments]\n" "$pgm"
181	printf "Options:\n"
182	printf "$optfmt" "-h" "Print this usage statement and exit."
183	printf "$optfmt" "-v" "Print version information and exit."
184	printf "Actions:\n"
185	for action in \
186		bridge		\
187		graph		\
188		pin		\
189		show		\
190		show1		\
191		shutdown	\
192		stats		\
193	; do
194		eval usage=\"\$jng_${action}_usage\"
195		[ "$usage" ] || continue
196		eval descr=\"\$jng_${action}_descr\"
197		printf "\t%s\n\t\t%s\n" "$usage" "$descr"
198	done
199	die
200}
201
202action_usage()
203{
204	local usage descr action="$1" fmt="$2"
205	shift 1 # action
206	if [ "$fmt" ]; then
207		shift 1 # fmt
208		printf "%s: %s: $fmt\n" "$pgm" "$action" "$@" >&2
209	fi
210	eval usage=\"\$jng_${action}_usage\"
211	printf "Usage: %s %s\n" "$pgm" "$usage" >&2
212	eval descr=\"\$jng_${action}_descr\"
213	printf "\t%s\n" "$descr" >&2
214	die
215}
216
217iface_encode()
218{
219	LC_ALL=C iface="$1" awk 'BEGIN {
220		for (n = 0; n < 256; n++)
221			pack[sprintf("%c", n)] = sprintf("_%02x", n)
222		numbers = "0123456789"
223		uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
224		lowercase = "abcdefghijklmnopqrstuvwxyz"
225		valid = "[" numbers uppercase lowercase "]"
226		iface = ENVIRON["iface"]
227		len = length(iface)
228		for (n = 1; n <= len; n++) {
229			let = substr(iface, n, 1)
230			_iface = _iface (let ~ valid ? let : pack[let])
231		}
232		print _iface
233	}'
234}
235
236derive_mac()
237{
238	local OPTIND=1 OPTARG __flag
239	local __mac_num= __make_pair=
240	while getopts 2n: __flag; do
241		case "$__flag" in
242		2) __make_pair=1 ;;
243		n) __mac_num=${OPTARG%%[^0-9]*} ;;
244		esac
245	done
246	shift $(( $OPTIND - 1 ))
247
248	local __iface="$1"
249	if [ ! "$__mac_num" ]; then
250		local __iface_encoded
251		__iface_encoded=$( iface_encode "$__iface" )
252		eval __mac_num=\${_${__iface_encoded}_num:--1}
253		__mac_num=$(( $__mac_num + 1 ))
254		eval _${__iface_encoded}_num=\$__mac_num
255	fi
256
257	local __name="$2" __var_to_set="$3" __var_to_set_b="$4"
258	local __iface_devid __new_devid __num __new_devid_b
259	#
260	# Calculate MAC address derived from given iface.
261	#
262	# The formula used is ``NP:SS:SS:II:II:II'' where:
263	# + N denotes 4 bits used as a counter to support branching
264	#   each parent interface up to 15 times under the same jail
265	#   name (see S below).
266	# + P denotes the special nibble whose value, if one of
267	#   2, 6, A, or E (but usually 2) denotes a privately
268	#   administered MAC address (while remaining routable).
269	# + S denotes 16 bits, the sum(1) value of the jail name.
270	# + I denotes bits that are inherited from parent interface.
271	#
272	# The S bits are a CRC-16 checksum of NAME, allowing the jail
273	# to change link numbers in ng_bridge(4) without effecting the
274	# MAC address. Meanwhile, if...
275	#   + the jail NAME changes (e.g., it was duplicated and given
276	#     a new name with no other changes)
277	#   + the underlying network interface changes
278	#   + the jail is moved to another host
279	# the MAC address will be recalculated to a new, similarly
280	# unique value preventing conflict.
281	#
282	__iface_devid=$( ifconfig $__iface ether | awk '/ether/,$0=$2' )
283	# ??:??:??:II:II:II
284	__new_devid=${__iface_devid#??:??:??} # => :II:II:II
285	# => :SS:SS:II:II:II
286	__num=$( set -- $( echo -n "$__name" | sum ) && echo $1 )
287	__new_devid=$( printf :%02x:%02x \
288		$(( $__num >> 8 & 255 )) $(( $__num & 255 )) )$__new_devid
289	# => P:SS:SS:II:II:II
290	case "$__iface_devid" in
291	   ?2:*) __new_devid=a$__new_devid __new_devid_b=e$__new_devid ;;
292	?[Ee]:*) __new_devid=2$__new_devid __new_devid_b=6$__new_devid ;;
293	      *) __new_devid=2$__new_devid __new_devid_b=e$__new_devid
294	esac
295	# => NP:SS:SS:II:II:II
296	__new_devid=$( printf %x $(( $__mac_num & 15 )) )$__new_devid
297	__new_devid_b=$( printf %x $(( $__mac_num & 15 )) )$__new_devid_b
298
299	#
300	# Return derivative MAC address(es)
301	#
302	if [ "$__make_pair" ]; then
303		if [ "$__var_to_set" -a "$__var_to_set_b" ]; then
304			eval $__var_to_set=\$__new_devid
305			eval $__var_to_set_b=\$__new_devid_b
306		else
307			echo $__new_devid $__new_devid_b
308		fi
309	else
310		if [ "$__var_to_set" ]; then
311			eval $__var_to_set=\$__new_devid
312		else
313			echo $__new_devid
314		fi
315	fi
316}
317
318mustberoot_to_continue()
319{
320	[ "$( id -u )" -eq 0 ] || die "Must run as root!"
321}
322
323jng_bridge_has_uplink()
324{
325	ngctl show "$1:" 2> /dev/null | awk '
326		$1 ~ /^uplink/ { found = 1; exit }
327		END { exit !found }
328	' # END-QUOTE
329}
330
331jng_bridge_persist_hosts()
332{
333	local node="$1"
334	local debug=0 loop=60 stable=1 config
335
336	eval $( ngctl msg "$node:" getconfig 2> /dev/null | awk '
337		{
338			if (match($0, /debugLevel=[0-9]+/))
339				printf "debug=%s ",
340					substr($0, RSTART + 11, RLENGTH - 11)
341			if (match($0, /loopTimeout=[0-9]+/))
342				printf "loop=%s ",
343					substr($0, RSTART + 12, RLENGTH - 12)
344			if (match($0, /minStableAge=[0-9]+/))
345				printf "stable=%s ",
346					substr($0, RSTART + 13, RLENGTH - 13)
347		}
348	' )
349	config="debugLevel=$debug"
350	config="$config loopTimeout=$loop"
351	config="$config maxStaleness=$NG_BRIDGE_MAX_STALENESS"
352	config="$config minStableAge=$stable"
353	quietly ngctl msg "$node:" setconfig "{ $config }"
354}
355
356jng_pin_mac()
357{
358	local node="$1" mac="$2" hook="$3"
359
360	[ "$node" -a "$mac" -a "$hook" ] || return $FAILURE
361	quietly ngctl msg "$node:" movehost "{ addr=$mac hook=\"$hook\" }"
362}
363
364jng_jiface_mac()
365{
366	local __jiface="$1" __jail="$2" __var_to_set="$3"
367	local __mac
368
369	__mac=$( ifconfig "$__jiface" ether 2> /dev/null |
370		awk '/ether/ { print $2; exit }' )
371	if [ ! "$__mac" -a "$__jail" ]; then
372		#
373		# After vnet.interface takes the eiface, it is gone
374		# from the host ifconfig; netgraph node remains.
375		#
376		__mac=$( jexec "$__jail" ifconfig "$__jiface" ether \
377			2> /dev/null | awk '/ether/ { print $2; exit }' )
378	fi
379	eval $__var_to_set=\"\$__mac\"
380	[ "$__mac" ]
381}
382
383jng_pin_jiface()
384{
385	local jiface="$1" jail="$2"
386	local mac peer peerhook pbridge phook
387
388	jng_jiface_mac "$jiface" "$jail" mac || return $FAILURE
389
390	# ether <peer> bridge <id> <peerhook>
391	set -- $( ngctl show "$jiface:" 2> /dev/null | awk '
392		$3 == "bridge" { print $2, $5; exit }
393	' )
394	peer="$1" peerhook="$2"
395	[ "$peer" -a "$peerhook" ] || return $FAILURE
396
397	jng_pin_mac "$peer" "$mac" "$peerhook" || return
398	if jng_bridge_has_uplink "$peer"; then
399		jng_bridge_persist_hosts "$peer" || : persist optional
400		return
401	fi
402
403	#
404	# Secondary bridge: also pin on the parent that holds uplink
405	# (restrictive unknown-unicast lives there).
406	#
407	set -- $( ngctl show "$peer:" 2> /dev/null | awk '
408		$3 == "bridge" { print $2, $5; exit }
409	' )
410	pbridge="$1" phook="$2"
411	[ "$pbridge" -a "$phook" ] || return $SUCCESS
412	jng_pin_mac "$pbridge" "$mac" "$phook" || return
413	jng_bridge_persist_hosts "$pbridge" || : persist optional
414}
415
416ng_ether_sanitize_ifname()
417{
418	# NB: Emulates function of same name in sys/netgraph/ng_ether.c
419	ifname="$1" awk 'BEGIN {
420		_ifname = ENVIRON["ifname"]
421		gsub(/[.:]/, "_", _ifname)
422		print _ifname
423	}'
424}
425
426jng_bridge_usage="bridge [-h] [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]"
427jng_bridge_descr="Create ng0_NAME [ng1_NAME ...]"
428jng_bridge()
429{
430	local OPTIND=1 OPTARG flag bridge=bridge
431	while getopts b:h flag; do
432		case "$flag" in
433		b) bridge="$OPTARG"
434		   [ "$bridge" ] ||
435			action_usage bridge "-b argument cannot be empty"
436			;; # NOTREACHED
437		*) action_usage bridge # NOTREACHED
438		esac
439	done
440	shift $(( $OPTIND - 1 ))
441
442	[ $# -gt 0 ] || action_usage bridge "too few arguments" # NOTREACHED
443
444	local name="$1"
445	[ "${name:-x}" = "${name#*[![:print:]]}" ] ||
446		action_usage bridge "invalid bridge name: %s" "$name"
447		# NOTREACHED
448	shift 1 # name
449
450	mustberoot_to_continue
451
452	local iface node parent jiface jiface_devid
453	local new clone_mac no_derive num quad mtu i=0
454	for iface in $*; do
455
456		clone_mac=
457		no_derive=
458		case "$iface" in
459		=*) iface=${iface#=} clone_mac=1 ;;
460		!*) iface=${iface#!} no_derive=1 ;;
461		esac
462
463		# ngctl(8) treats `.' and `:' as control characters, so
464		# ng_ether(4) names its node after the sanitized ifname
465		node=$( ng_ether_sanitize_ifname "$iface" )
466
467		# Make sure the interface doesn't exist already
468		jiface=ng${i}_$name
469		if quietly ngctl msg "$jiface:" getifname; then
470			i=$(( $i + 1 ))
471			continue
472		fi
473
474		# Bring the interface up
475		ifconfig $iface up || return
476
477		# Set promiscuous mode and don't overwrite src addr
478		ngctl msg $node: setpromisc 1 || return
479		ngctl msg $node: setautosrc 0 || return
480
481		# Make sure the interface has been bridged
482		# NB: You must connect uplinkX before linkX
483		# NB: see ng_bridge(4) for policy on first connected hook
484		if ! quietly ngctl info ${node}bridge:; then
485			ngctl mkpeer $node: bridge lower uplink1 || return
486			ngctl connect $node: $node:lower upper link0 ||
487				return
488			ngctl name $node:lower ${node}bridge || return
489			jng_bridge_persist_hosts ${node}bridge ||
490				: persist optional
491		fi
492
493		mtu=$( ifconfig $iface | sed -n '1s/^.*mtu //p' ) || return
494
495		# Optionally create a secondary bridge
496		# NB: This time, you want to only connect linkX (no uplinkX)
497		if [ "$bridge" != "bridge" ] &&
498		   ! quietly ngctl info "$node$bridge:"
499		then
500			num=1
501			while quietly ngctl msg ${node}bridge: getstats $num
502			do
503				num=$(( $num + 1 ))
504			done
505			ngctl mkpeer $node:lower bridge link$num link0 ||
506				return
507			ngctl name ${node}bridge:link$num "$node$bridge" ||
508				return
509		fi
510
511		# Create a new interface to the bridge
512		num=1
513		while quietly ngctl msg "$node$bridge:" getstats $num; do
514			num=$(( $num + 1 ))
515		done
516		ngctl mkpeer "$node$bridge:" eiface link$num ether || return
517
518		# Rename the new interface
519		while [ ${#jiface} -gt 15 ]; do # OS limitation
520			jiface=${jiface%?}
521		done
522		new=$( ngctl show -n "$node$bridge:link$num" ) || return
523		new=$( set -- $new; echo $2 )
524		ngctl name "$node$bridge:link$num" $jiface || return
525		ifconfig $new name $jiface || return
526		ifconfig $jiface mtu $mtu || return
527		ifconfig $jiface up || return
528
529		#
530		# Set the MAC address of the new interface using a sensible
531		# algorithm to prevent conflicts on the network.
532		#
533		jiface_devid=
534		if [ "$clone_mac" ]; then
535			jiface_devid=$( ifconfig $iface ether |
536				awk '/ether/,$0=$2' )
537		elif [ ! "$no_derive" ]; then
538			derive_mac $iface "$name" jiface_devid
539		fi
540		[ "$jiface_devid" ] &&
541			quietly ifconfig $jiface ether $jiface_devid
542		jng_pin_jiface "$jiface" "$name" || : pin optional
543
544		i=$(( $i + 1 ))
545	done # for iface
546}
547
548jng_pin_usage="pin [-h] {-a | NAME ...}"
549jng_pin_descr="Pin eiface MACs into ng_bridge forwarding database (FDB)"
550jng_pin()
551{
552	local OPTIND=1 OPTARG flag
553	local show_all= err=$SUCCESS
554	local name iface node jiface
555
556	while getopts ah flag; do
557		case "$flag" in
558		a) show_all=1 ;;
559		*) action_usage pin # NOTREACHED
560		esac
561	done
562	shift $(( $OPTIND - 1 ))
563	if [ "$show_all" ]; then
564		[ $# -eq 0 ] ||
565			action_usage pin "too many arguments" # NOTREACHED
566		for iface in $( ifconfig -l ); do
567			node=$( ng_ether_sanitize_ifname "$iface" )
568			quietly ngctl info ${node}bridge: || continue
569			jng_bridge_persist_hosts ${node}bridge ||
570				: persist optional
571		done
572		set -- $( jls -q name 2> /dev/null )
573		[ $# -gt 0 ] ||
574			action_usage pin "no jails" # NOTREACHED
575	else
576		[ $# -gt 0 ] ||
577			action_usage pin "too few arguments" # NOTREACHED
578	fi
579
580	mustberoot_to_continue
581
582	for name in "$@"; do
583		[ "${name:-x}" = "${name#*[![:print:]]}" ] ||
584			action_usage pin "invalid name: %s" "$name"
585			# NOTREACHED
586		for jiface in $( jexec "$name" ifconfig -l 2> /dev/null )
587		do
588			case "$jiface" in
589			ng[0-9]*)
590				jng_pin_jiface "$jiface" "$name" || {
591					echo "$pgm: pin $jiface: failed" >&2
592					err=$FAILURE
593				}
594				;;
595			esac
596		done
597	done
598	return $err
599}
600
601jng_graph_usage="graph [-fh] [-T type] [-o output]"
602jng_graph_descr="Generate network graph (default output is 'jng.svg')"
603jng_graph()
604{
605	local OPTIND=1 OPTARG flag
606	local output=jng.svg output_type= force=
607	while getopts fho:T: flag; do
608		case "$flag" in
609		f) force=1 ;;
610		o) output="$OPTARG" ;;
611		T) output_type="$OPTARG" ;;
612		*) action_usage graph # NOTREACHED
613		esac
614	done
615	shift $(( $OPTIND - 1 ))
616
617	[ $# -eq 0 ] || action_usage graph "too many arguments" # NOTREACHED
618
619	mustberoot_to_continue
620
621	if [ -e "$output" -a ! "$force" ]; then
622		echo "$output: Already exists (use '-f' to overwrite)" >&2
623		return $FAILURE
624	fi
625	if [ ! "$output_type" ]; then
626		local valid suffix
627		valid=$( dot -Txxx 2>&1 )
628		for suffix in ${valid##*:}; do
629			[ "$output" != "${output%.$suffix}" ] || continue
630			output_type=$suffix
631			break
632		done
633	fi
634	ngctl dot | dot ${output_type:+-T "$output_type"} -o "$output"
635}
636
637jng_show_usage="show [-h]"
638jng_show_descr="List possible NAME values for 'show NAME'"
639jng_show1_usage="show [-h] NAME ..."
640jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]"
641jng_show2_usage="show [NAME ...]"
642jng_show2_descr="List NAME values or show interfaces associated with NAME."
643jng_show()
644{
645	local OPTIND=1 OPTARG flag
646	local name
647	while getopts h flag; do
648		case "$flag" in
649		*) action_usage show2 # NOTREACHED
650		esac
651	done
652	shift $(( $OPTIND - 1 ))
653
654	mustberoot_to_continue
655
656	if [ $# -eq 0 ]; then
657		ngctl ls | awk '$4=="bridge",$0=$2' |
658			xargs -rn1 -Ibridge ngctl show bridge: |
659			awk 'sub(/^ng[[:digit:]]+_/, "", $2), $0 = $2' |
660			sort -u
661		return
662	fi
663	for name in "$@"; do
664		ngctl ls | awk -v name="$name" '
665			match($2, /^ng[[:digit:]]+_/) &&
666				substr($2, RSTART + RLENGTH) == name &&
667				$4 == "eiface", $0 = $2
668		' | sort
669	done
670}
671
672jng_shutdown_usage="shutdown [-h] NAME ..."
673jng_shutdown_descr="Shutdown ng0_NAME [ng1_NAME ...]"
674jng_shutdown()
675{
676	local OPTIND=1 OPTARG flag
677	while getopts h flag; do
678		case "$flag" in
679		*) action_usage shutdown # NOTREACHED
680		esac
681	done
682	shift $(( $OPTIND -1 ))
683
684	[ $# -gt 0 ] || action_usage shutdown "too few arguments" # NOTREACHED
685
686	mustberoot_to_continue
687
688	local name
689	for name in "$@"; do
690		[ "${name:-x}" = "${name#*[![:print:]]}" ] ||
691			action_usage shutdown "invalid name: %s" "$name"
692			# NOTREACHED
693		jng_show "$name" | xargs -rn1 -I jiface ngctl shutdown jiface:
694	done
695}
696
697jng_stats_usage="stats [-hj] {-a | NAME ...}"
698jng_stats_descr="Show ng_bridge link statistics for NAME interfaces"
699jng_stats()
700{
701	local OPTIND=1 OPTARG flag
702	local show_all=
703	local name iface node ether=
704	while getopts ahj flag; do
705		case "$flag" in
706		a) show_all=1 ;;
707		j) STATS_FMT=json
708			export pgm
709			: "${HOSTNAME:=$( hostname )}"
710			export HOSTNAME
711			;;
712		*) action_usage stats # NOTREACHED
713		esac
714	done
715	shift $(( $OPTIND -1 ))
716	if [ "$show_all" ]; then
717		[ $# -eq 0 ] ||
718			action_usage stats "too many arguments" # NOTREACHED
719
720		# Get a list of bridged ng_ether(4) devices
721		for iface in $( ifconfig -l ); do
722			node=$( ng_ether_sanitize_ifname "$iface" )
723			quietly ngctl info ${node}bridge: || continue
724			ether="$ether $iface"
725		done
726		set -- $ether $( "$0" show )
727		[ $# -gt 0 ] ||
728			action_usage stats "no bridged interfaces" # NOTREACHED
729	else
730		[ $# -gt 0 ] ||
731			action_usage stats "too few arguments" # NOTREACHED
732	fi
733
734	mustberoot_to_continue
735
736	local now="$( date +%s )"
737	for name in "$@"; do
738		[ "${name:-x}" = "${name#*[![:print:]]}" ] ||
739			action_usage stats "invalid name: %s" "$name"
740			# NOTREACHED
741		if ifconfig -l | xargs -n1 2> /dev/null | fgrep -qw "$name"
742		then
743			node=$( ng_ether_sanitize_ifname "$name" )
744			[ "$STATS_FMT" != "text" ] ||
745				echo "${node}bridge:uplink1 [lower]"
746			ngctl msg ${node}bridge: getstats -1 |
747				fmt_stats -n "${name}.lower" -t "$now"
748
749			[ "$STATS_FMT" != "text" ] ||
750				echo "${node}bridge:link0 [upper]"
751			ngctl msg ${node}bridge: getstats 0 |
752				fmt_stats -n "${name}.upper" -t "$now"
753		fi
754		local jiface
755		for jiface in $( jng_show "$name" ); do
756			[ "$STATS_FMT" != "text" ] || echo "$jiface:"
757			ngctl show $jiface: | awk '
758			$3 == "bridge" && $5 ~ /^link/ {
759				bridge = $2
760				link = substr($5, 5)
761				system(sprintf("ngctl msg %s: getstats %u",
762					bridge, link))
763			}' | fmt_stats -n "$jiface" -t "$now"
764		done
765	done
766}
767fmt_stats()
768{
769	local OPTIND=1 OPTARG flag
770	local time=
771	while getopts n:t: flag; do
772		case "$flag" in
773		n) name="$OPTARG" ;;
774		t) time="$OPTARG" ;;
775		*) break
776		esac
777	done
778	shift $(( OPTIND - 1 ))
779	fmt 2 | awk -v fmt="$STATS_FMT" -v name="$name" -v tm="$time" '
780		function json_add_str(pre, k, s)
781		{
782			return sprintf("%s,\"%s\":\"%s\"", pre, k, s)
783		}
784		function json_add_int(pre, k, i)
785		{
786			return sprintf("%s,\"%s\":%d", pre, k, i)
787		}
788		BEGIN {
789			if (fmt == "json") {
790				if (tm == "") srand() # Time-seed
791				js = json_add_int(js, "epoch",
792					tm != "" ? tm : srand())
793				js = json_add_str(js, "hostname",
794					ENVIRON["HOSTNAME"])
795				js = json_add_str(js, "program",
796					ENVIRON["pgm"])
797				js = json_add_str(js, "name", name)
798			}
799		}
800		/=/ && fl = index($0, "=") {
801			key = substr($0, 0, fl-1)
802			val = substr($0, fl+1)
803			if (fmt == "json") {
804				js = json_add_int(js, key, val)
805			} else { # Multi-line text
806				printf "%20s = %s\n", key, val
807			}
808		}
809		END {
810			if (fmt == "json") {
811				print "{" substr(js, 2) "}"
812			}
813		}
814	' # END-QUOTE
815}
816
817############################################################ MAIN
818
819#
820# Command-line arguments
821#
822[ $# -gt 0 ] || usage "too few arguments" # NOTREACHED
823action="$1"
824[ "$action" ] || usage # NOTREACHED
825
826#
827# Validate action argument
828#
829case "$action" in
830-h) usage ;; # NOTREACHED
831-v) VERSION="${VERSION#*: }"
832	echo "${VERSION% $}"
833	exit $SUCCESS ;;
834-*) usage "unknown option: %s" "$action" ;; # NOTREACHED
835*[!a-zA-Z0-9_-]*) usage 'invalid action "%s"' "$action" ;; # NOTREACHED
836esac
837if [ "$BASH_VERSION" ]; then
838	type="$( type -t "jng_$action" )"
839else
840	type="$( type "jng_$action" 2> /dev/null )"
841fi || usage 'unknown action "%s"' "$action" # NOTREACHED
842case "$type" in
843*function)
844	shift 1 # action
845	eval "jng_$action" \"\$@\"
846	;;
847*) usage 'unknown action "%s"' "$action" # NOTREACHED
848esac
849
850################################################################################
851# END
852################################################################################
853