New version of jng (2.0)

Changes for jng 1.0 -> 2.0 include:

+ Add experimental MSS clamping
+ Add support for ng_bridge(4) NGM_BRIDGE_GET_STATS (getstats)
+ Add JSON formatted ng_bridge(4) statistics (see above) via "jng stats -j <name>"
+ Add error messages
+ Minor refactoring for code readability (read: quietly() function)
+ Rename eiface variables to jiface to clarify as-for jail interface (not ng_eiface(4))
+ Fix missing description for alternate form of "jng show" usage
+ Update "jng show <name>" to accept multiple names (now "jng show <name> …" is allowed)
+ Update "jng shutdown <name>" to accept multiple names (now "jng shutdown <name> …" is allowed)
+ Add "-a" option to "jng stats" (as-in "jng stats -a") to show all ng_bridge(4) stats
+ Update "jng stats <name>" to accept any kind of name (make it easier to use)
+ Add version ident
+ Remove extraneous line in LICENSE section
+ Add -h to usage statements
+ Bump copyright

Reviewed by:	jlduran
Differential Revision:	https://reviews.freebsd.org/D43516
This commit is contained in:
Devin Teske
2026-04-04 19:39:22 +00:00
parent e55db843ef
commit 8e68f940c1
+249 -70
View File
@@ -1,6 +1,6 @@
#!/bin/sh
#-
# Copyright (c) 2016 Devin Teske
# Copyright (c) 2016-2024 Devin Teske <dteske@FreeBSD.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@@ -24,10 +24,10 @@
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
############################################################ IDENT(1)
#
# $Title: netgraph(4) management script for vnet jails $
# $Version: 2.0 $
#
############################################################ INFORMATION
#
@@ -129,6 +129,26 @@
# NB: While this tool can't create every type of desirable topology, it should
# handle most setups, minus some which considered exotic or purpose-built.
#
############################################################ CONFIGURATION
#
# Netgraph node type. Can be `iface' or `eiface' and refers to whether
# ng_iface(4) or ng_eiface(4) is used with ng_bridge(4). The advantages of
# choosing iface over eiface is that with iface you can utilize ng_tcpmss(4)
# to limit the TCP MSS for operating in environments that clamp down on ICMP.
#
# NB: iface/tcpmss support is EXPERIMENTAL
#
NG_TYPE=eiface # Can be iface or eiface
#
# Clamp TCP Maximum Segment Size to reasonably below standard MTU
# NB: Fixes TCP hangup issue in environments where ICMP is restricted
# NB: Be liberal about MSS (RFC 879, section 7)
# NB: Unused unless NG_TYPE=iface
#
NG_TCPMSS_CONFIG='{ inHook="bridge" outHook="'$NG_TYPE'" maxMSS=1280 }'
############################################################ GLOBALS
pgm="${0##*/}" # Program basename
@@ -139,13 +159,27 @@ pgm="${0##*/}" # Program basename
SUCCESS=0
FAILURE=1
#
# Command-line options
#
STATS_FMT=text # -j for JSON
############################################################ FUNCTIONS
quietly(){ "$@" > /dev/null 2>&1; }
usage()
{
local fmt="$1"
local action usage descr
exec >&2
echo "Usage: $pgm action [arguments]"
if [ "$fmt" ]; then
shift 1 # fmt
printf "%s: $fmt\n" "$pgm" "$@"
fi
echo "Usage: $pgm [-h] action [arguments]"
echo "Options:"
printf "\t-h Print usage statement and exit.\n"
echo "Actions:"
for action in \
bridge \
@@ -165,7 +199,12 @@ usage()
action_usage()
{
local usage descr action="$1"
local usage descr action="$1" fmt="$2"
shift 1 # action
if [ "$fmt" ]; then
shift 1 # fmt
printf "%s: %s: $fmt\n" "$pgm" "$action" "$@" >&2
fi
eval usage=\"\$jng_${action}_usage\"
echo "Usage: $pgm $usage" >&2
eval descr=\"\$jng_${action}_descr\"
@@ -260,28 +299,33 @@ mustberoot_to_continue()
fi
}
jng_bridge_usage="bridge [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]"
jng_bridge_usage="bridge [-h] [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]"
jng_bridge_descr="Create ng0_NAME [ng1_NAME ...]"
jng_bridge()
{
local OPTIND=1 OPTARG flag bridge=bridge
while getopts b: flag; do
while getopts b:h flag; do
case "$flag" in
b) bridge="$OPTARG"
[ "$bridge" ] || action_usage bridge ;; # NOTREACHED
[ "$bridge" ] ||
action_usage bridge "-b argument cannot be empty"
;; # NOTREACHED
*) action_usage bridge # NOTREACHED
esac
done
shift $(( $OPTIND - 1 ))
[ $# -gt 0 ] || action_usage bridge "too few arguments" # NOTREACHED
local name="$1"
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -gt 1 ] ||
action_usage bridge # NOTREACHED
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] ||
action_usage bridge "invalid bridge name: %s" "$name"
# NOTREACHED
shift 1 # name
mustberoot_to_continue
local iface parent eiface eiface_devid
local iface parent jiface jiface_devid
local new clone_mac no_derive num quad i=0
for iface in $*; do
@@ -293,8 +337,8 @@ jng_bridge()
esac
# Make sure the interface doesn't exist already
eiface=ng${i}_$name
if ngctl msg "$eiface:" getifname > /dev/null 2>&1; then
jiface=ng${i}_$name
if quietly ngctl msg "$jiface:" getifname; then
i=$(( $i + 1 ))
continue
fi
@@ -307,7 +351,7 @@ jng_bridge()
ngctl msg $iface: setautosrc 0 || return
# Make sure the interface has been bridged
if ! ngctl info ${iface}bridge: > /dev/null 2>&1; then
if ! quietly ngctl info ${iface}bridge:; then
ngctl mkpeer $iface: bridge lower link0 || return
ngctl connect $iface: $iface:lower upper link1 ||
return
@@ -318,11 +362,10 @@ jng_bridge()
# Optionally create a secondary bridge
if [ "$bridge" != "bridge" ] &&
! ngctl info "$iface$bridge:" > /dev/null 2>&1
! quietly ngctl info "$iface$bridge:"
then
num=2
while ngctl msg ${iface}bridge: getstats $num \
> /dev/null 2>&1
while quietly ngctl msg ${iface}bridge: getstats $num
do
num=$(( $num + 1 ))
done
@@ -334,48 +377,80 @@ jng_bridge()
# Create a new interface to the bridge
num=2
while ngctl msg "$iface$bridge:" getstats $num > /dev/null 2>&1
do
while quietly ngctl msg "$iface$bridge:" getstats $num; do
num=$(( $num + 1 ))
done
ngctl mkpeer "$iface$bridge:" eiface link$num ether || return
local hook peerhook
case "$NG_TYPE" in
eiface)
# Hook the eiface directly to the bridge
hook=link$num peerhook=ether
ngctl mkpeer "$iface$bridge:" \
$NG_TYPE $hook $peerhook || return
;;
iface)
# Hook tcpmss<->iface to bridge
hook=link$num peerhook=bridge
ngctl mkpeer "$iface$bridge:" \
tcpmss $hook $peerhook || return
hook=iface peerhook=inet
ngctl mkpeer "$iface$bridge:link$num" \
$NG_TYPE $hook $peerhook || return
;;
*) return $FAILURE
esac
# Rename the new interface
while [ ${#eiface} -gt 15 ]; do # OS limitation
eiface=${eiface%?}
while [ ${#jiface} -gt 15 ]; do # OS limitation
jiface=${jiface%?}
done
new=$( set -- `ngctl show -n "$iface$bridge:link$num"` &&
echo $2 ) || return
ngctl name "$iface$bridge:link$num" $eiface || return
ifconfig $new name $eiface || return
ifconfig $eiface mtu $mtu || return
ifconfig $eiface up || return
case "$NG_TYPE" in
eiface)
new=$( ngctl show -n "$iface$bridge:link$num" ) ||
return
new=$( set -- $new; echo $2 )
ngctl name "$iface$bridge:link$num" $jiface || return
;;
iface)
ngctl name "$iface$bridge:link$num" $jiface-mss ||
return
new=$( ngctl show -n "$jiface-mss:$hook" ) || return
new=$( set -- $new; echo $2 )
ngctl name $jiface-mss:$hook $jiface || return
ngctl msg $jiface: broadcast || return
ngctl msg $jiface-mss: config "$NG_TCPMSS_CONFIG" ||
return
;;
esac
ifconfig $new name $jiface || return
ifconfig $jiface mtu $mtu || return
ifconfig $jiface up || return
#
# Set the MAC address of the new interface using a sensible
# algorithm to prevent conflicts on the network.
#
eiface_devid=
jiface_devid=
if [ "$clone_mac" ]; then
eiface_devid=$( ifconfig $iface ether |
jiface_devid=$( ifconfig $iface ether |
awk '/ether/,$0=$2' )
elif [ ! "$no_derive" ]; then
derive_mac $iface "$name" eiface_devid
derive_mac $iface "$name" jiface_devid
fi
[ "$eiface_devid" ] &&
ifconfig $eiface ether $eiface_devid > /dev/null 2>&1
[ "$jiface_devid" ] &&
quietly ifconfig $jiface ether $jiface_devid
i=$(( $i + 1 ))
done # for iface
}
jng_graph_usage="graph [-f] [-T type] [-o output]"
jng_graph_usage="graph [-fh] [-T type] [-o output]"
jng_graph_descr="Generate network graph (default output is \`jng.svg')"
jng_graph()
{
local OPTIND=1 OPTARG flag
local output=jng.svg output_type= force=
while getopts fo:T: flag; do
while getopts fho:T: flag; do
case "$flag" in
f) force=1 ;;
o) output="$OPTARG" ;;
@@ -384,8 +459,11 @@ jng_graph()
esac
done
shift $(( $OPTIND - 1 ))
[ $# -eq 0 -a "$output" ] || action_usage graph # NOTREACHED
[ $# -eq 0 ] || action_usage graph "too many arguments" # NOTREACHED
mustberoot_to_continue
if [ -e "$output" -a ! "$force" ]; then
echo "$output: Already exists (use \`-f' to overwrite)" >&2
return $FAILURE
@@ -402,21 +480,25 @@ jng_graph()
ngctl dot | dot ${output_type:+-T "$output_type"} -o "$output"
}
jng_show_usage="show"
jng_show_usage="show [-h]"
jng_show_descr="List possible NAME values for \`show NAME'"
jng_show1_usage="show NAME"
jng_show1_usage="show [-h] NAME ..."
jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]"
jng_show2_usage="show [NAME]"
jng_show2_usage="show [NAME ...]"
jng_show2_descr="List NAME values or show interfaces associated with NAME."
jng_show()
{
local OPTIND=1 OPTARG flag
while getopts "" flag; do
local name
while getopts h flag; do
case "$flag" in
*) action_usage show2 # NOTREACHED
esac
done
shift $(( $OPTIND - 1 ))
mustberoot_to_continue
if [ $# -eq 0 ]; then
ngctl ls | awk '$4=="bridge",$0=$2' |
xargs -rn1 -Ibridge ngctl show bridge: |
@@ -424,69 +506,166 @@ jng_show()
sort -u
return
fi
ngctl ls | awk -v name="$1" '
match($2, /^ng[[:digit:]]+_/) &&
substr($2, RSTART + RLENGTH) == name &&
$4 == "eiface", $0 = $2
' | sort
for name in "$@"; do
ngctl ls | awk -v name="$name" '
BEGIN { N = length(name) + 1 }
!match(ng = $2, /^ng[[:digit:]]+_/) { next }
{ _name = substr(ng, S = RSTART + RLENGTH) }
_name != name && substr(_name, 1, N) != name "-" { next }
(type = $4) ~ /^(e?iface|tcpmss)$/, $0 = ng
' | sort
done
}
jng_shutdown_usage="shutdown NAME"
jng_shutdown_usage="shutdown [-h] NAME ..."
jng_shutdown_descr="Shutdown ng0_NAME [ng1_NAME ...]"
jng_shutdown()
{
local OPTIND=1 OPTARG flag
while getopts "" flag; do
while getopts h flag; do
case "$flag" in
*) action_usage shutdown # NOTREACHED
esac
done
shift $(( $OPTIND -1 ))
local name="$1"
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -eq 1 ] ||
action_usage shutdown # NOTREACHED
[ $# -gt 0 ] || action_usage shutdown "too few arguments" # NOTREACHED
mustberoot_to_continue
jng_show "$name" | xargs -rn1 -I eiface ngctl shutdown eiface:
local name
for name in "$@"; do
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] ||
action_usage shutdown "invalid name: %s" "$name"
# NOTREACHED
jng_show "$name" | xargs -rn1 -I jiface ngctl shutdown jiface:
done
}
jng_stats_usage="stats NAME"
jng_stats_usage="stats [-hj] {-a | NAME ...}"
jng_stats_descr="Show ng_bridge link statistics for NAME interfaces"
jng_stats()
{
local OPTIND=1 OPTARG flag
while getopts "" flag; do
local show_all=
local name iface ether=
while getopts ahj flag; do
case "$flag" in
a) show_all=1 ;;
j) STATS_FMT=json
export pgm
: "${HOSTNAME:=$( hostname )}"
export HOSTNAME
;;
*) action_usage stats # NOTREACHED
esac
done
shift $(( $OPTIND -1 ))
local name="$1"
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" -a $# -eq 1 ] ||
action_usage stats # NOTREACHED
if [ "$show_all" ]; then
[ $# -eq 0 ] ||
action_usage stats "too many arguments" # NOTREACHED
# Get a list of bridged ng_ether(4) devices
for iface in $( ifconfig -l ); do
quietly ngctl info ${iface}bridge: || continue
ether="$ether $iface"
done
set -- $ether $( "$0" show )
[ $# -gt 0 ] ||
action_usage stats "no bridged interfaces" # NOTREACHED
else
[ $# -gt 0 ] ||
action_usage stats "too few arguments" # NOTREACHED
fi
mustberoot_to_continue
for eiface in $( jng_show "$name" ); do
echo "$eiface:"
ngctl show $eiface: | awk '
$3 == "bridge" && $5 ~ /^link/ {
bridge = $2
link = substr($5, 5)
system(sprintf("ngctl msg %s: getstats %u",
bridge, link))
}' | fmt 2 | awk '
/=/ && fl = index($0, "=") {
printf "%20s = %s\n",
substr($0, 0, fl-1),
substr($0, 0, fl+1)
}
' # END-QUOTE
local now="$( date +%s )"
for name in "$@"; do
[ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] ||
action_usage stats "invalid name: %s" "$name"
# NOTREACHED
if ifconfig -l | xargs -n1 2> /dev/null | fgrep -qw "$name"
then
[ "$STATS_FMT" != "text" ] ||
echo "${name}bridge:link0 [lower]"
ngctl msg ${name}bridge: getstats 0 |
fmt_stats -n "${name}.lower" -t "$now"
[ "$STATS_FMT" != "text" ] ||
echo "${name}bridge:link0 [lower]"
ngctl msg ${name}bridge: getstats 1 |
fmt_stats -n "${name}.upper" -t "$now"
fi
local jiface
for jiface in $( jng_show "$name" ); do
[ "$STATS_FMT" != "text" ] || echo "$jiface:"
ngctl show $jiface: | awk '
$3 == "bridge" && $5 ~ /^link/ {
bridge = $2
link = substr($5, 5)
system(sprintf("ngctl msg %s: getstats %u",
bridge, link))
}' | fmt_stats -n "$jiface" -t "$now"
done
done
}
fmt_stats()
{
local OPTIND=1 OPTARG flag
local time=
while getopts n:t: flag; do
case "$flag" in
n) name="$OPTARG" ;;
t) time="$OPTARG" ;;
*) break
esac
done
shift $(( OPTIND - 1 ))
fmt 2 | awk -v fmt="$STATS_FMT" -v name="$name" -v tm="$time" '
function json_add_str(pre, k, s)
{
return sprintf("%s,\"%s\":\"%s\"", pre, k, s)
}
function json_add_int(pre, k, i)
{
return sprintf("%s,\"%s\":%d", pre, k, i)
}
BEGIN {
if (fmt == "json") {
if (tm == "") srand() # Time-seed
js = json_add_int(js, "epoch",
tm != "" ? tm : srand())
js = json_add_str(js, "hostname",
ENVIRON["HOSTNAME"])
js = json_add_str(js, "program",
ENVIRON["pgm"])
js = json_add_str(js, "name", name)
}
}
/=/ && fl = index($0, "=") {
key = substr($0, 0, fl-1)
val = substr($0, fl+1)
if (fmt == "json") {
js = json_add_int(js, key, val)
} else { # Multi-line text
printf "%20s = %s\n", key, val
}
}
END {
if (fmt == "json") {
print "{" substr(js, 2) "}"
}
}
' # END-QUOTE
}
############################################################ MAIN
#
# Command-line arguments
#
[ $# -gt 0 ] || usage "too few arguments" # NOTREACHED
action="$1"
[ "$action" ] || usage # NOTREACHED