git: 644990f193cc - main - New version of jng (9.0)
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 18 Aug 2026 18:56:38 UTC
The branch main has been updated by dteske:
URL: https://cgit.FreeBSD.org/src/commit/?id=644990f193cc284db241125e567519cafa1c7ccd
commit 644990f193cc284db241125e567519cafa1c7ccd
Author: Devin Teske <dteske@FreeBSD.org>
AuthorDate: 2026-08-18 18:54:36 +0000
Commit: Devin Teske <dteske@FreeBSD.org>
CommitDate: 2026-08-18 18:54:36 +0000
New version of jng (9.0)
Changes for jng 2.0 -> 9.0 include:
+ Use ng_bridge(4) uplink hooks on ng_ether(4) lower so the host
mapping table stays small (first hook is uplink; unknown unicast
goes only to uplink)
+ Add `jng pin [-h] {-a | NAME ...}' to plant eiface MACs with
NGM_BRIDGE_MOVE_HOST and raise maxStaleness so they do not expire
+ Remove experimental NG_TYPE=iface / ng_tcpmss(4); ng_iface(4)
cannot work with ng_bridge(4)
+ Add -v
+ SPDX-License-Identifier: BSD-2-Clause; bump copyright to 2026
See D58902 for the ng_bridge(4) data-path MOVE_HOST fix.
MFC after: 1 week
Reviewed by: kfv, jlduran
Differential Revision: https://reviews.freebsd.org/D58903
---
share/examples/jails/jng | 347 +++++++++++++++++++++++++++++++----------------
1 file changed, 232 insertions(+), 115 deletions(-)
diff --git a/share/examples/jails/jng b/share/examples/jails/jng
index 03f5c318199c..82f1dd7b5e94 100755
--- a/share/examples/jails/jng
+++ b/share/examples/jails/jng
@@ -1,33 +1,13 @@
#!/bin/sh
-#-
-# Copyright (c) 2016-2024 Devin Teske <dteske@FreeBSD.org>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2016-2026 Devin Teske <dteske@FreeBSD.org>
#
############################################################ IDENT(1)
#
# $Title: netgraph(4) management script for vnet jails $
-# $Version: 2.0 $
+# $Version: 9.0 $
#
############################################################ INFORMATION
#
@@ -84,6 +64,7 @@
# ### BEGIN EXCERPT ###
#
# jail_enable="YES"
+# #jail_confwarn="NO" # Optional: disable warning to migrate to jail.conf(5)
# jail_list="xxx"
#
# #
@@ -127,30 +108,32 @@
# jail_{name}_devfs_ruleset="11" (rc.conf(5)).
#
# 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.
+# handle most setups, minus some considered exotic or purpose-built.
+#
+# Uplink on ng_ether(4) `lower' (jng 7+) keeps the WAN MAC table small:
+# ng_bridge(4) does not learn on uplink hooks. The first connected hook being
+# uplink also selects restrictive unknown-unicast: frames for an unknown dest
+# go only to uplink, not to jail links. Inbound unicast to a jail therefore
+# requires that jail's MAC to live in the forwarding database (FDB) on the
+# jail's link. jng 8 pins each eiface MAC with ngctl movehost and sets
+# maxStaleness so host->staleness cannot catch it. `jng pin NAME' replants
+# after an accidental move. ng_bridge must not MOVE_HOST from learnMac=0 hooks
+# or promiscuous TX echo can steal a pinned MAC onto uplink; without that
+# kernel fix, re-run `jng pin'.
#
############################################################ 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
+# host->staleness is uint16_t; conf.maxStaleness is uint32_t.
+# ng_bridge_timeout expires when ++staleness >= maxStaleness.
+# A threshold above 65535 is unreachable (the counter wraps).
#
-NG_TCPMSS_CONFIG='{ inHook="bridge" outHook="'$NG_TYPE'" maxMSS=1280 }'
+NG_BRIDGE_MAX_STALENESS=4294967295
############################################################ GLOBALS
+VERSION='$Version: 9.0 $'
+
pgm="${0##*/}" # Program basename
#
@@ -168,22 +151,35 @@ STATS_FMT=text # -j for JSON
quietly(){ "$@" > /dev/null 2>&1; }
+die()
+{
+ local fmt="$1"
+ if [ "$fmt" ]; then
+ shift 1 # fmt
+ printf "%s: $fmt\n" "$pgm" "$@" >&2
+ fi
+ exit $FAILURE
+}
+
usage()
{
local fmt="$1"
+ local optfmt="\t%-5s %s\n"
local action usage descr
exec >&2
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:"
+ printf "Usage: %s [-hv] action [arguments]\n" "$pgm"
+ printf "Options:\n"
+ printf "$optfmt" "-h" "Print this usage statement and exit."
+ printf "$optfmt" "-v" "Print version information and exit."
+ printf "Actions:\n"
for action in \
bridge \
graph \
+ pin \
show \
show1 \
shutdown \
@@ -194,7 +190,7 @@ usage()
eval descr=\"\$jng_${action}_descr\"
printf "\t%s\n\t\t%s\n" "$usage" "$descr"
done
- exit $FAILURE
+ die
}
action_usage()
@@ -206,10 +202,10 @@ action_usage()
printf "%s: %s: $fmt\n" "$pgm" "$action" "$@" >&2
fi
eval usage=\"\$jng_${action}_usage\"
- echo "Usage: $pgm $usage" >&2
+ printf "Usage: %s %s\n" "$pgm" "$usage" >&2
eval descr=\"\$jng_${action}_descr\"
- printf "\t%s\n" "$descr"
- exit $FAILURE
+ printf "\t%s\n" "$descr" >&2
+ die
}
derive_mac()
@@ -235,7 +231,7 @@ derive_mac()
#
# Calculate MAC address derived from given iface.
#
- # The formula I'm using is ``NP:SS:SS:II:II:II'' where:
+ # The formula used is ``NP:SS:SS:II:II:II'' where:
# + N denotes 4 bits used as a counter to support branching
# each parent interface up to 15 times under the same jail
# name (see S below).
@@ -246,7 +242,7 @@ derive_mac()
# + I denotes bits that are inherited from parent interface.
#
# The S bits are a CRC-16 checksum of NAME, allowing the jail
- # to change link numbers in ng_bridge(4) without affecting the
+ # to change link numbers in ng_bridge(4) without effecting the
# MAC address. Meanwhile, if...
# + the jail NAME changes (e.g., it was duplicated and given
# a new name with no other changes)
@@ -259,7 +255,7 @@ derive_mac()
# ??:??:??:II:II:II
__new_devid=${__iface_devid#??:??:??} # => :II:II:II
# => :SS:SS:II:II:II
- __num=$( set -- `echo -n "$__name" | sum` && echo $1 )
+ __num=$( set -- $( echo -n "$__name" | sum ) && echo $1 )
__new_devid=$( printf :%02x:%02x \
$(( $__num >> 8 & 255 )) $(( $__num & 255 )) )$__new_devid
# => P:SS:SS:II:II:II
@@ -293,10 +289,100 @@ derive_mac()
mustberoot_to_continue()
{
- if [ "$( id -u )" -ne 0 ]; then
- echo "Must run as root!" >&2
- exit $FAILURE
+ [ "$( id -u )" -eq 0 ] || die "Must run as root!"
+}
+
+jng_bridge_has_uplink()
+{
+ ngctl show "$1:" 2> /dev/null | awk '
+ $1 ~ /^uplink/ { found = 1; exit }
+ END { exit !found }
+ ' # END-QUOTE
+}
+
+jng_bridge_persist_hosts()
+{
+ local node="$1"
+ local debug=0 loop=60 stable=1 config
+
+ eval $( ngctl msg "$node:" getconfig 2> /dev/null | awk '
+ {
+ if (match($0, /debugLevel=[0-9]+/))
+ printf "debug=%s ",
+ substr($0, RSTART + 11, RLENGTH - 11)
+ if (match($0, /loopTimeout=[0-9]+/))
+ printf "loop=%s ",
+ substr($0, RSTART + 12, RLENGTH - 12)
+ if (match($0, /minStableAge=[0-9]+/))
+ printf "stable=%s ",
+ substr($0, RSTART + 13, RLENGTH - 13)
+ }
+ ' )
+ config="debugLevel=$debug"
+ config="$config loopTimeout=$loop"
+ config="$config maxStaleness=$NG_BRIDGE_MAX_STALENESS"
+ config="$config minStableAge=$stable"
+ quietly ngctl msg "$node:" setconfig "{ $config }"
+}
+
+jng_pin_mac()
+{
+ local node="$1" mac="$2" hook="$3"
+
+ [ "$node" -a "$mac" -a "$hook" ] || return $FAILURE
+ quietly ngctl msg "$node:" movehost "{ addr=$mac hook=\"$hook\" }"
+}
+
+jng_jiface_mac()
+{
+ local __jiface="$1" __jail="$2" __var_to_set="$3"
+ local __mac
+
+ __mac=$( ifconfig "$__jiface" ether 2> /dev/null |
+ awk '/ether/ { print $2; exit }' )
+ if [ ! "$__mac" -a "$__jail" ]; then
+ #
+ # After vnet.interface takes the eiface, it is gone
+ # from the host ifconfig; netgraph node remains.
+ #
+ __mac=$( jexec "$__jail" ifconfig "$__jiface" ether \
+ 2> /dev/null | awk '/ether/ { print $2; exit }' )
fi
+ eval $__var_to_set=\"\$__mac\"
+ [ "$__mac" ]
+}
+
+jng_pin_jiface()
+{
+ local jiface="$1" jail="$2"
+ local mac peer peerhook pbridge phook
+
+ jng_jiface_mac "$jiface" "$jail" mac || return $FAILURE
+
+ # ether <peer> bridge <id> <peerhook>
+ set -- $( ngctl show "$jiface:" 2> /dev/null | awk '
+ $3 == "bridge" { print $2, $5; exit }
+ ' )
+ peer="$1" peerhook="$2"
+ [ "$peer" -a "$peerhook" ] || return $FAILURE
+
+ jng_pin_mac "$peer" "$mac" "$peerhook" || return
+ if jng_bridge_has_uplink "$peer"; then
+ jng_bridge_persist_hosts "$peer" || : persist optional
+ return
+ fi
+
+ #
+ # Secondary bridge: also pin on the parent that holds uplink
+ # (restrictive unknown-unicast lives there).
+ #
+ set -- $( ngctl show "$peer:" 2> /dev/null | awk '
+ $3 == "bridge" { print $2, $5; exit }
+ ' )
+ pbridge="$1" phook="$2"
+ [ "$pbridge" -a "$phook" ] || return $SUCCESS
+ jng_pin_mac "$pbridge" "$mac" "$phook" || return
+ jng_bridge_persist_hosts "$pbridge" || : persist optional
}
jng_bridge_usage="bridge [-h] [-b BRIDGE_NAME] NAME [!|=]iface0 [[!|=]iface1 ...]"
@@ -326,7 +412,7 @@ jng_bridge()
mustberoot_to_continue
local iface parent jiface jiface_devid
- local new clone_mac no_derive num quad i=0
+ local new clone_mac no_derive num quad mtu i=0
for iface in $*; do
clone_mac=
@@ -351,77 +437,49 @@ jng_bridge()
ngctl msg $iface: setautosrc 0 || return
# Make sure the interface has been bridged
+ # NB: You must connect uplinkX before linkX
+ # NB: see ng_bridge(4) for policy on first connected hook
if ! quietly ngctl info ${iface}bridge:; then
- ngctl mkpeer $iface: bridge lower link0 || return
- ngctl connect $iface: $iface:lower upper link1 ||
+ ngctl mkpeer $iface: bridge lower uplink1 || return
+ ngctl connect $iface: $iface:lower upper link0 ||
return
ngctl name $iface:lower ${iface}bridge || return
+ jng_bridge_persist_hosts ${iface}bridge ||
+ : persist optional
fi
- mtu=$(ifconfig ${iface} | sed -n '1s/^.*mtu //p;') || return
+ mtu=$( ifconfig $iface | sed -n '1s/^.*mtu //p' ) || return
# Optionally create a secondary bridge
+ # NB: This time, you want to only connect linkX (no uplinkX)
if [ "$bridge" != "bridge" ] &&
! quietly ngctl info "$iface$bridge:"
then
- num=2
+ num=1
while quietly ngctl msg ${iface}bridge: getstats $num
do
num=$(( $num + 1 ))
done
- ngctl mkpeer $iface:lower bridge link$num link1 ||
+ ngctl mkpeer $iface:lower bridge link$num link0 ||
return
ngctl name ${iface}bridge:link$num "$iface$bridge" ||
return
fi
# Create a new interface to the bridge
- num=2
+ num=1
while quietly ngctl msg "$iface$bridge:" getstats $num; do
num=$(( $num + 1 ))
done
- 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
+ ngctl mkpeer "$iface$bridge:" eiface link$num ether || return
# Rename the new interface
while [ ${#jiface} -gt 15 ]; do # OS limitation
jiface=${jiface%?}
done
- 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
+ new=$( ngctl show -n "$iface$bridge:link$num" ) || return
+ new=$( set -- $new; echo $2 )
+ ngctl name "$iface$bridge:link$num" $jiface || return
ifconfig $new name $jiface || return
ifconfig $jiface mtu $mtu || return
ifconfig $jiface up || return
@@ -439,13 +497,66 @@ jng_bridge()
fi
[ "$jiface_devid" ] &&
quietly ifconfig $jiface ether $jiface_devid
+ jng_pin_jiface "$jiface" "$name" || : pin optional
i=$(( $i + 1 ))
done # for iface
}
+jng_pin_usage="pin [-h] {-a | NAME ...}"
+jng_pin_descr="Pin eiface MACs into ng_bridge forwarding database (FDB)"
+jng_pin()
+{
+ local OPTIND=1 OPTARG flag
+ local show_all= err=$SUCCESS
+ local name iface jiface
+
+ while getopts ah flag; do
+ case "$flag" in
+ a) show_all=1 ;;
+ *) action_usage pin # NOTREACHED
+ esac
+ done
+ shift $(( $OPTIND - 1 ))
+ if [ "$show_all" ]; then
+ [ $# -eq 0 ] ||
+ action_usage pin "too many arguments" # NOTREACHED
+ for iface in $( ifconfig -l ); do
+ quietly ngctl info ${iface}bridge: || continue
+ jng_bridge_persist_hosts ${iface}bridge ||
+ : persist optional
+ done
+ set -- $( jls -q name 2> /dev/null )
+ [ $# -gt 0 ] ||
+ action_usage pin "no jails" # NOTREACHED
+ else
+ [ $# -gt 0 ] ||
+ action_usage pin "too few arguments" # NOTREACHED
+ fi
+
+ mustberoot_to_continue
+
+ for name in "$@"; do
+ [ "${name:-x}" = "${name#*[!0-9a-zA-Z_]}" ] ||
+ action_usage pin "invalid name: %s" "$name"
+ # NOTREACHED
+ for jiface in $( jexec "$name" ifconfig -l 2> /dev/null )
+ do
+ case "$jiface" in
+ ng[0-9]*)
+ jng_pin_jiface "$jiface" "$name" || {
+ echo "$pgm: pin $jiface: failed" >&2
+ err=$FAILURE
+ }
+ ;;
+ esac
+ done
+ done
+ return $err
+}
+
jng_graph_usage="graph [-fh] [-T type] [-o output]"
-jng_graph_descr="Generate network graph (default output is \`jng.svg')"
+jng_graph_descr="Generate network graph (default output is 'jng.svg')"
jng_graph()
{
local OPTIND=1 OPTARG flag
@@ -465,7 +576,7 @@ jng_graph()
mustberoot_to_continue
if [ -e "$output" -a ! "$force" ]; then
- echo "$output: Already exists (use \`-f' to overwrite)" >&2
+ echo "$output: Already exists (use '-f' to overwrite)" >&2
return $FAILURE
fi
if [ ! "$output_type" ]; then
@@ -481,7 +592,7 @@ jng_graph()
}
jng_show_usage="show [-h]"
-jng_show_descr="List possible NAME values for \`show NAME'"
+jng_show_descr="List possible NAME values for 'show NAME'"
jng_show1_usage="show [-h] NAME ..."
jng_show1_descr="Lists ng0_NAME [ng1_NAME ...]"
jng_show2_usage="show [NAME ...]"
@@ -508,11 +619,9 @@ jng_show()
fi
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
+ match($2, /^ng[[:digit:]]+_/) &&
+ substr($2, RSTART + RLENGTH) == name &&
+ $4 == "eiface", $0 = $2
' | sort
done
}
@@ -588,13 +697,13 @@ jng_stats()
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 |
+ echo "${name}bridge:uplink1 [lower]"
+ ngctl msg ${name}bridge: getstats -1 |
fmt_stats -n "${name}.lower" -t "$now"
[ "$STATS_FMT" != "text" ] ||
- echo "${name}bridge:link1 [upper]"
- ngctl msg ${name}bridge: getstats 1 |
+ echo "${name}bridge:link0 [upper]"
+ ngctl msg ${name}bridge: getstats 0 |
fmt_stats -n "${name}.upper" -t "$now"
fi
local jiface
@@ -672,17 +781,25 @@ action="$1"
#
# Validate action argument
#
+case "$action" in
+-h) usage ;; # NOTREACHED
+-v) VERSION="${VERSION#*: }"
+ echo "${VERSION% $}"
+ exit $SUCCESS ;;
+-*) usage "unknown option: %s" "$action" ;; # NOTREACHED
+*[!a-zA-Z0-9_-]*) usage 'invalid action "%s"' "$action" ;; # NOTREACHED
+esac
if [ "$BASH_VERSION" ]; then
- type="$( type -t "jng_$action" )" || usage # NOTREACHED
+ type="$( type -t "jng_$action" )"
else
- type="$( type "jng_$action" 2> /dev/null )" || usage # NOTREACHED
-fi
+ type="$( type "jng_$action" 2> /dev/null )"
+fi || usage 'unknown action "%s"' "$action" # NOTREACHED
case "$type" in
*function)
shift 1 # action
eval "jng_$action" \"\$@\"
;;
-*) usage # NOTREACHED
+*) usage 'unknown action "%s"' "$action" # NOTREACHED
esac
################################################################################