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